-
Notifications
You must be signed in to change notification settings - Fork 1.4k
feat: add legacy /chat/completions support #1804
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
4280732
cf1290d
87a5801
2fd6286
d2841c1
5316583
6b75973
48b9335
6f01035
9243cc5
e19a7e0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| --- | ||
| "@browserbasehq/stagehand": minor | ||
| "@browserbasehq/stagehand-server-v3": minor | ||
| "@browserbasehq/stagehand-server-v4": minor | ||
| --- | ||
|
|
||
| feat: add `chatcompletions` provider prefix and `modelBaseURL` support for OpenAI-compatible endpoints | ||
|
|
||
| Adds a `chatcompletions/` model name prefix that forces the Chat Completions API (`/chat/completions`) instead of the Responses API (`/responses`), enabling support for OpenAI-compatible providers like ZhipuAI GLM. Also adds `modelBaseURL` support end-to-end: client SDK sends `x-model-base-url` header, both server-v3 and server-v4 extract and thread it, and Stainless generates it as an optional parameter across all language SDKs. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -172,70 +172,129 @@ You must respond in JSON format. respond WITH JSON. Do not include any other tex | |
| }); | ||
| } | ||
|
|
||
| try { | ||
| objectResponse = await generateObject({ | ||
| // chatcompletions/ models (provider: "openai.chat") can't do structured | ||
| // output — skip schema entirely to avoid a wasted LLM call. | ||
| // Other fallback models (deepseek, kimi) succeed with schema. | ||
| let useNoSchema = | ||
| needsPromptJsonFallback && this.model.provider === "openai.chat"; | ||
|
|
||
| if (!useNoSchema) { | ||
| try { | ||
| objectResponse = await generateObject({ | ||
| model: this.model, | ||
| messages: formattedMessages, | ||
| schema: options.response_model.schema, | ||
| temperature, | ||
| providerOptions: isGPT5 | ||
| ? { | ||
| openai: { | ||
| textVerbosity: isCodex ? "medium" : "low", // codex models only support 'medium' | ||
| reasoningEffort: isCodex | ||
| ? "medium" | ||
| : usesLowReasoningEffort | ||
| ? "low" | ||
| : "minimal", | ||
| }, | ||
| } | ||
| : undefined, | ||
| }); | ||
| } catch (err) { | ||
| if (needsPromptJsonFallback) { | ||
| useNoSchema = true; | ||
| } else { | ||
| // Log error response to maintain request/response pairing | ||
| SessionFileLogger.logLlmResponse({ | ||
| requestId: llmRequestId, | ||
| model: this.model.modelId, | ||
| operation: "generateObject", | ||
| output: `[error: ${err instanceof Error ? err.message : "unknown"}]`, | ||
| }); | ||
|
|
||
| if (NoObjectGeneratedError.isInstance(err)) { | ||
| this.logger?.({ | ||
| category: "AISDK error", | ||
| message: err.message, | ||
| level: 0, | ||
| auxiliary: { | ||
| cause: { | ||
| value: JSON.stringify(err.cause ?? {}), | ||
| type: "object", | ||
| }, | ||
| text: { | ||
| value: err.text ?? "", | ||
| type: "string", | ||
| }, | ||
| response: { | ||
| value: JSON.stringify(err.response ?? {}), | ||
| type: "object", | ||
| }, | ||
| usage: { | ||
| value: JSON.stringify(err.usage ?? {}), | ||
| type: "object", | ||
| }, | ||
| finishReason: { | ||
| value: err.finishReason ?? "unknown", | ||
| type: "string", | ||
| }, | ||
| requestId: { | ||
| value: options.requestId, | ||
| type: "string", | ||
| }, | ||
| }, | ||
| }); | ||
| } | ||
|
|
||
| throw err; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // No-schema fallback for models that can't do structured output. | ||
| // Pipeline: call LLM → fix strings → fix missing arrays → validate | ||
| if (useNoSchema) { | ||
| // 1. Call LLM without schema (prompt instruction guides JSON output) | ||
| const noSchemaResponse = await generateObject({ | ||
| model: this.model, | ||
| messages: formattedMessages, | ||
| schema: options.response_model.schema, | ||
| output: "no-schema", | ||
| temperature, | ||
| providerOptions: isGPT5 | ||
| ? { | ||
| openai: { | ||
| textVerbosity: isCodex ? "medium" : "low", // codex models only support 'medium' | ||
| reasoningEffort: isCodex | ||
| ? "medium" | ||
| : usesLowReasoningEffort | ||
| ? "low" | ||
| : "minimal", | ||
| }, | ||
| } | ||
| : undefined, | ||
| }); | ||
| } catch (err) { | ||
| // Log error response to maintain request/response pairing | ||
| SessionFileLogger.logLlmResponse({ | ||
| requestId: llmRequestId, | ||
| model: this.model.modelId, | ||
| operation: "generateObject", | ||
| output: `[error: ${err instanceof Error ? err.message : "unknown"}]`, | ||
| }); | ||
|
|
||
| if (NoObjectGeneratedError.isInstance(err)) { | ||
| this.logger?.({ | ||
| category: "AISDK error", | ||
| message: err.message, | ||
| level: 0, | ||
| auxiliary: { | ||
| cause: { | ||
| value: JSON.stringify(err.cause ?? {}), | ||
| type: "object", | ||
| }, | ||
| text: { | ||
| value: err.text ?? "", | ||
| type: "string", | ||
| }, | ||
| response: { | ||
| value: JSON.stringify(err.response ?? {}), | ||
| type: "object", | ||
| }, | ||
| usage: { | ||
| value: JSON.stringify(err.usage ?? {}), | ||
| type: "object", | ||
| }, | ||
| finishReason: { | ||
| value: err.finishReason ?? "unknown", | ||
| type: "string", | ||
| }, | ||
| requestId: { | ||
| value: options.requestId, | ||
| type: "string", | ||
| }, | ||
| }, | ||
| }); | ||
|
|
||
| throw err; | ||
| // 2. Fix strings — models may return "[]" instead of [] | ||
| const raw = noSchemaResponse.object as Record<string, unknown>; | ||
| for (const [k, v] of Object.entries(raw)) { | ||
| if (typeof v === "string") { | ||
| try { | ||
| raw[k] = JSON.parse(v); | ||
| } catch { | ||
| // keep as string | ||
| } | ||
| } | ||
| } | ||
| // 3. Fix missing arrays — models may omit empty array fields entirely | ||
| let parsed: unknown; | ||
| const firstTry = options.response_model.schema.safeParse(raw); | ||
| if (firstTry.success) { | ||
| parsed = firstTry.data; | ||
| } else { | ||
| for (const issue of firstTry.error.issues) { | ||
| if ( | ||
| issue.code === "invalid_type" && | ||
| issue.expected === "array" && | ||
| issue.path.length === 1 | ||
| ) { | ||
| raw[issue.path[0] as string] = []; | ||
| } | ||
| } | ||
| // 4. Validate against schema | ||
| const secondTry = options.response_model.schema.safeParse(raw); | ||
| if (!secondTry.success) { | ||
| throw new Error( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P1: Custom agent: Exception and error message sanitization Generic Prompt for AI agents |
||
| `Model response could not be coerced into the expected schema: ${secondTry.error.message}`, | ||
| ); | ||
| } | ||
| parsed = secondTry.data; | ||
| } | ||
| throw err; | ||
| objectResponse = { ...noSchemaResponse, object: parsed }; | ||
| } | ||
|
|
||
| const result = { | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.