feat: support multiple providers via Responses-Completion transformation (#247)

https://github.com/user-attachments/assets/9ecb51be-fa65-4e99-8512-abb898dda569

Implemented it as a transformation between Responses API and Completion
API so that it supports existing providers that implement the Completion
API and minimizes the changes needed to the codex repo.

---------

Co-authored-by: Thibault Sottiaux <tibo@openai.com>
Co-authored-by: Fouad Matin <169186268+fouad-openai@users.noreply.github.com>
Co-authored-by: Fouad Matin <fouad@openai.com>
This commit is contained in:
Daniel Nakov
2025-04-20 23:59:34 -04:00
committed by GitHub
parent 693bd59ecc
commit eafbc75612
11 changed files with 1870 additions and 83 deletions

View File

@@ -1,16 +1,19 @@
import type { ReviewDecision } from "./review.js";
import type { ApplyPatchCommand, ApprovalPolicy } from "../../approvals.js";
import type { AppConfig } from "../config.js";
import type { ResponseEvent } from "../responses.js";
import type {
ResponseFunctionToolCall,
ResponseInputItem,
ResponseItem,
ResponseCreateParams,
} from "openai/resources/responses/responses.mjs";
import type { Reasoning } from "openai/resources.mjs";
import { log } from "./log.js";
import { OPENAI_BASE_URL, OPENAI_TIMEOUT_MS } from "../config.js";
import { OPENAI_TIMEOUT_MS, getApiKey, getBaseUrl } from "../config.js";
import { parseToolCallArguments } from "../parsers.js";
import { responsesCreateViaChatCompletions } from "../responses.js";
import {
ORIGIN,
CLI_VERSION,
@@ -39,6 +42,7 @@ const alreadyProcessedResponses = new Set();
type AgentLoopParams = {
model: string;
provider?: string;
config?: AppConfig;
instructions?: string;
approvalPolicy: ApprovalPolicy;
@@ -58,6 +62,7 @@ type AgentLoopParams = {
export class AgentLoop {
private model: string;
private provider: string;
private instructions?: string;
private approvalPolicy: ApprovalPolicy;
private config: AppConfig;
@@ -198,6 +203,7 @@ export class AgentLoop {
// private cumulativeThinkingMs = 0;
constructor({
model,
provider = "openai",
instructions,
approvalPolicy,
// `config` used to be required. Some unittests (and potentially other
@@ -214,6 +220,7 @@ export class AgentLoop {
additionalWritableRoots,
}: AgentLoopParams & { config?: AppConfig }) {
this.model = model;
this.provider = provider;
this.instructions = instructions;
this.approvalPolicy = approvalPolicy;
@@ -236,7 +243,9 @@ export class AgentLoop {
this.sessionId = getSessionId() || randomUUID().replaceAll("-", "");
// Configure OpenAI client with optional timeout (ms) from environment
const timeoutMs = OPENAI_TIMEOUT_MS;
const apiKey = this.config.apiKey ?? process.env["OPENAI_API_KEY"] ?? "";
const apiKey = getApiKey(this.provider);
const baseURL = getBaseUrl(this.provider);
this.oai = new OpenAI({
// The OpenAI JS SDK only requires `apiKey` when making requests against
// the official API. When running unittests we stub out all network
@@ -245,7 +254,7 @@ export class AgentLoop {
// errors inside the SDK (it validates that `apiKey` is a nonempty
// string when the field is present).
...(apiKey ? { apiKey } : {}),
baseURL: OPENAI_BASE_URL,
baseURL,
defaultHeaders: {
originator: ORIGIN,
version: CLI_VERSION,
@@ -492,11 +501,23 @@ export class AgentLoop {
const mergedInstructions = [prefix, this.instructions]
.filter(Boolean)
.join("\n");
const responseCall =
!this.config.provider ||
this.config.provider?.toLowerCase() === "openai"
? (params: ResponseCreateParams) =>
this.oai.responses.create(params)
: (params: ResponseCreateParams) =>
responsesCreateViaChatCompletions(
this.oai,
params as ResponseCreateParams & { stream: true },
);
log(
`instructions (length ${mergedInstructions.length}): ${mergedInstructions}`,
);
// eslint-disable-next-line no-await-in-loop
stream = await this.oai.responses.create({
stream = await responseCall({
model: this.model,
instructions: mergedInstructions,
previous_response_id: lastResponseId || undefined,
@@ -720,7 +741,7 @@ export class AgentLoop {
try {
// eslint-disable-next-line no-await-in-loop
for await (const event of stream) {
for await (const event of stream as AsyncIterable<ResponseEvent>) {
log(`AgentLoop.run(): response event ${event.type}`);
// process and surface each item (noop until we can depend on streaming events)