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:
@@ -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 unit‑tests (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 unit‑tests we stub out all network
|
||||
@@ -245,7 +254,7 @@ export class AgentLoop {
|
||||
// errors inside the SDK (it validates that `apiKey` is a non‑empty
|
||||
// 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 (no‑op until we can depend on streaming events)
|
||||
|
||||
Reference in New Issue
Block a user