| 33 | } from "./openaiResponses.js"; |
| 34 | |
| 35 | export class OpenAIApi implements BaseLlmApi { |
| 36 | openai: OpenAI; |
| 37 | apiBase: string = "https://api.openai.com/v1/"; |
| 38 | |
| 39 | constructor(protected config: z.infer<typeof OpenAIConfigSchema>) { |
| 40 | this.apiBase = config.apiBase ?? this.apiBase; |
| 41 | |
| 42 | // Always create the original OpenAI client for fallback |
| 43 | this.openai = new OpenAI({ |
| 44 | // Necessary because `new OpenAI()` will throw an error if there is no API Key |
| 45 | apiKey: config.apiKey ?? "", |
| 46 | baseURL: this.apiBase, |
| 47 | fetch: customFetch(config.requestOptions), |
| 48 | timeout: config?.requestOptions?.timeout || undefined, |
| 49 | }); |
| 50 | } |
| 51 | modifyChatBody<T extends ChatCompletionCreateParams>(body: T): T { |
| 52 | // Add stream_options to include usage in streaming responses |
| 53 | if (body.stream) { |
| 54 | (body as any).stream_options = { include_usage: true }; |
| 55 | } |
| 56 | |
| 57 | // DeepSeek reasoner models use max_completion_tokens instead of max_tokens |
| 58 | if ( |
| 59 | body.max_tokens && |
| 60 | (this.apiBase?.includes("api.deepseek.com") || |
| 61 | body.model.includes("deepseek-reasoner")) |
| 62 | ) { |
| 63 | body.max_completion_tokens = body.max_tokens; |
| 64 | body.max_tokens = undefined; |
| 65 | } |
| 66 | |
| 67 | // o-series models - only apply for official OpenAI API |
| 68 | const isOfficialOpenAIAPI = this.apiBase === "https://api.openai.com/v1/"; |
| 69 | if (isOfficialOpenAIAPI) { |
| 70 | if (body.model.startsWith("o") || body.model.includes("gpt-5")) { |
| 71 | // a) use max_completion_tokens instead of max_tokens |
| 72 | body.max_completion_tokens = body.max_tokens; |
| 73 | body.max_tokens = undefined; |
| 74 | |
| 75 | // b) use "developer" message role rather than "system" |
| 76 | body.messages = body.messages.map((message) => { |
| 77 | if (message.role === "system") { |
| 78 | return { ...message, role: "developer" } as any; |
| 79 | } |
| 80 | return message; |
| 81 | }); |
| 82 | } |
| 83 | if (body.tools?.length && !body.model.startsWith("o3")) { |
| 84 | body.parallel_tool_calls = false; |
| 85 | } |
| 86 | } |
| 87 | return body; |
| 88 | } |
| 89 | |
| 90 | protected shouldUseResponsesEndpoint(model: string): boolean { |
| 91 | if (this.config.useResponsesApi === false) { |
| 92 | return false; |
nothing calls this directly
no outgoing calls
no test coverage detected