mirror of
https://github.com/anomalyco/opencode.git
synced 2026-05-21 19:02:07 +00:00
1770 lines
57 KiB
TypeScript
1770 lines
57 KiB
TypeScript
import {
|
|
APICallError,
|
|
type JSONValue,
|
|
type LanguageModelV3,
|
|
type LanguageModelV3CallOptions,
|
|
type LanguageModelV3Content,
|
|
type LanguageModelV3ProviderTool,
|
|
type LanguageModelV3StreamPart,
|
|
type SharedV3ProviderMetadata,
|
|
type SharedV3Warning,
|
|
} from "@ai-sdk/provider"
|
|
import {
|
|
combineHeaders,
|
|
createEventSourceResponseHandler,
|
|
createJsonResponseHandler,
|
|
generateId,
|
|
parseProviderOptions,
|
|
type ParseResult,
|
|
postJsonToApi,
|
|
} from "@ai-sdk/provider-utils"
|
|
import { z } from "zod/v4"
|
|
import type { OpenAIConfig } from "./openai-config"
|
|
import { openaiFailedResponseHandler } from "./openai-error"
|
|
import { codeInterpreterInputSchema, codeInterpreterOutputSchema } from "./tool/code-interpreter"
|
|
import { fileSearchOutputSchema } from "./tool/file-search"
|
|
import { imageGenerationOutputSchema } from "./tool/image-generation"
|
|
import { convertToOpenAIResponsesInput } from "./convert-to-openai-responses-input"
|
|
import { mapOpenAIResponseFinishReason } from "./map-openai-responses-finish-reason"
|
|
import type { OpenAIResponsesIncludeOptions, OpenAIResponsesIncludeValue } from "./openai-responses-api-types"
|
|
import { prepareResponsesTools } from "./openai-responses-prepare-tools"
|
|
import type { OpenAIResponsesModelId } from "./openai-responses-settings"
|
|
import { localShellInputSchema } from "./tool/local-shell"
|
|
|
|
const webSearchCallItem = z.object({
|
|
type: z.literal("web_search_call"),
|
|
id: z.string(),
|
|
status: z.string(),
|
|
action: z
|
|
.discriminatedUnion("type", [
|
|
z.object({
|
|
type: z.literal("search"),
|
|
query: z.string().nullish(),
|
|
}),
|
|
z.object({
|
|
type: z.literal("open_page"),
|
|
url: z.string(),
|
|
}),
|
|
z.object({
|
|
type: z.literal("find"),
|
|
url: z.string(),
|
|
pattern: z.string(),
|
|
}),
|
|
])
|
|
.nullish(),
|
|
})
|
|
|
|
const fileSearchCallItem = z.object({
|
|
type: z.literal("file_search_call"),
|
|
id: z.string(),
|
|
queries: z.array(z.string()),
|
|
results: z
|
|
.array(
|
|
z.object({
|
|
attributes: z.record(z.string(), z.unknown()),
|
|
file_id: z.string(),
|
|
filename: z.string(),
|
|
score: z.number(),
|
|
text: z.string(),
|
|
}),
|
|
)
|
|
.nullish(),
|
|
})
|
|
|
|
const codeInterpreterCallItem = z.object({
|
|
type: z.literal("code_interpreter_call"),
|
|
id: z.string(),
|
|
code: z.string().nullable(),
|
|
container_id: z.string(),
|
|
outputs: z
|
|
.array(
|
|
z.discriminatedUnion("type", [
|
|
z.object({ type: z.literal("logs"), logs: z.string() }),
|
|
z.object({ type: z.literal("image"), url: z.string() }),
|
|
]),
|
|
)
|
|
.nullable(),
|
|
})
|
|
|
|
const localShellCallItem = z.object({
|
|
type: z.literal("local_shell_call"),
|
|
id: z.string(),
|
|
call_id: z.string(),
|
|
action: z.object({
|
|
type: z.literal("exec"),
|
|
command: z.array(z.string()),
|
|
timeout_ms: z.number().optional(),
|
|
user: z.string().optional(),
|
|
working_directory: z.string().optional(),
|
|
env: z.record(z.string(), z.string()).optional(),
|
|
}),
|
|
})
|
|
|
|
const imageGenerationCallItem = z.object({
|
|
type: z.literal("image_generation_call"),
|
|
id: z.string(),
|
|
result: z.string(),
|
|
})
|
|
|
|
/**
|
|
* `top_logprobs` request body argument can be set to an integer between
|
|
* 0 and 20 specifying the number of most likely tokens to return at each
|
|
* token position, each with an associated log probability.
|
|
*
|
|
* @see https://platform.openai.com/docs/api-reference/responses/create#responses_create-top_logprobs
|
|
*/
|
|
const TOP_LOGPROBS_MAX = 20
|
|
|
|
const LOGPROBS_SCHEMA = z.array(
|
|
z.object({
|
|
token: z.string(),
|
|
logprob: z.number(),
|
|
top_logprobs: z.array(
|
|
z.object({
|
|
token: z.string(),
|
|
logprob: z.number(),
|
|
}),
|
|
),
|
|
}),
|
|
)
|
|
|
|
export class OpenAIResponsesLanguageModel implements LanguageModelV3 {
|
|
readonly specificationVersion = "v3"
|
|
|
|
readonly modelId: OpenAIResponsesModelId
|
|
|
|
private readonly config: OpenAIConfig
|
|
|
|
constructor(modelId: OpenAIResponsesModelId, config: OpenAIConfig) {
|
|
this.modelId = modelId
|
|
this.config = config
|
|
}
|
|
|
|
readonly supportedUrls: Record<string, RegExp[]> = {
|
|
"image/*": [/^https?:\/\/.*$/],
|
|
"application/pdf": [/^https?:\/\/.*$/],
|
|
}
|
|
|
|
get provider(): string {
|
|
return this.config.provider
|
|
}
|
|
|
|
private async getArgs({
|
|
maxOutputTokens,
|
|
temperature,
|
|
stopSequences,
|
|
topP,
|
|
topK,
|
|
presencePenalty,
|
|
frequencyPenalty,
|
|
seed,
|
|
prompt,
|
|
providerOptions,
|
|
tools,
|
|
toolChoice,
|
|
responseFormat,
|
|
}: LanguageModelV3CallOptions) {
|
|
const warnings: SharedV3Warning[] = []
|
|
const modelConfig = getResponsesModelConfig(this.modelId)
|
|
|
|
if (topK != null) {
|
|
warnings.push({ type: "unsupported", feature: "topK" })
|
|
}
|
|
|
|
if (seed != null) {
|
|
warnings.push({ type: "unsupported", feature: "seed" })
|
|
}
|
|
|
|
if (presencePenalty != null) {
|
|
warnings.push({
|
|
type: "unsupported",
|
|
feature: "presencePenalty",
|
|
})
|
|
}
|
|
|
|
if (frequencyPenalty != null) {
|
|
warnings.push({
|
|
type: "unsupported",
|
|
feature: "frequencyPenalty",
|
|
})
|
|
}
|
|
|
|
if (stopSequences != null) {
|
|
warnings.push({ type: "unsupported", feature: "stopSequences" })
|
|
}
|
|
|
|
const openaiOptions = await parseProviderOptions({
|
|
provider: "copilot",
|
|
providerOptions,
|
|
schema: openaiResponsesProviderOptionsSchema,
|
|
})
|
|
|
|
const { input, warnings: inputWarnings } = await convertToOpenAIResponsesInput({
|
|
prompt,
|
|
systemMessageMode: modelConfig.systemMessageMode,
|
|
fileIdPrefixes: this.config.fileIdPrefixes,
|
|
store: openaiOptions?.store ?? true,
|
|
hasLocalShellTool: hasOpenAITool("openai.local_shell"),
|
|
})
|
|
|
|
warnings.push(...inputWarnings)
|
|
|
|
const strictJsonSchema = openaiOptions?.strictJsonSchema ?? false
|
|
|
|
let include: OpenAIResponsesIncludeOptions = openaiOptions?.include
|
|
|
|
function addInclude(key: OpenAIResponsesIncludeValue) {
|
|
include = include != null ? [...include, key] : [key]
|
|
}
|
|
|
|
function hasOpenAITool(id: string) {
|
|
return tools?.find((tool) => tool.type === "provider" && tool.id === id) != null
|
|
}
|
|
|
|
// when logprobs are requested, automatically include them:
|
|
const topLogprobs =
|
|
typeof openaiOptions?.logprobs === "number"
|
|
? openaiOptions?.logprobs
|
|
: openaiOptions?.logprobs === true
|
|
? TOP_LOGPROBS_MAX
|
|
: undefined
|
|
|
|
if (topLogprobs) {
|
|
addInclude("message.output_text.logprobs")
|
|
}
|
|
|
|
// when a web search tool is present, automatically include the sources:
|
|
const webSearchToolName = (
|
|
tools?.find(
|
|
(tool) =>
|
|
tool.type === "provider" && (tool.id === "openai.web_search" || tool.id === "openai.web_search_preview"),
|
|
) as LanguageModelV3ProviderTool | undefined
|
|
)?.name
|
|
|
|
if (webSearchToolName) {
|
|
addInclude("web_search_call.action.sources")
|
|
}
|
|
|
|
// when a code interpreter tool is present, automatically include the outputs:
|
|
if (hasOpenAITool("openai.code_interpreter")) {
|
|
addInclude("code_interpreter_call.outputs")
|
|
}
|
|
|
|
const baseArgs = {
|
|
model: this.modelId,
|
|
input,
|
|
temperature,
|
|
top_p: topP,
|
|
max_output_tokens: maxOutputTokens,
|
|
|
|
...((responseFormat?.type === "json" || openaiOptions?.textVerbosity) && {
|
|
text: {
|
|
...(responseFormat?.type === "json" && {
|
|
format:
|
|
responseFormat.schema != null
|
|
? {
|
|
type: "json_schema",
|
|
strict: strictJsonSchema,
|
|
name: responseFormat.name ?? "response",
|
|
description: responseFormat.description,
|
|
schema: responseFormat.schema,
|
|
}
|
|
: { type: "json_object" },
|
|
}),
|
|
...(openaiOptions?.textVerbosity && {
|
|
verbosity: openaiOptions.textVerbosity,
|
|
}),
|
|
},
|
|
}),
|
|
|
|
// provider options:
|
|
max_tool_calls: openaiOptions?.maxToolCalls,
|
|
metadata: openaiOptions?.metadata,
|
|
parallel_tool_calls: openaiOptions?.parallelToolCalls,
|
|
previous_response_id: openaiOptions?.previousResponseId,
|
|
store: openaiOptions?.store,
|
|
user: openaiOptions?.user,
|
|
instructions: openaiOptions?.instructions,
|
|
service_tier: openaiOptions?.serviceTier,
|
|
include,
|
|
prompt_cache_key: openaiOptions?.promptCacheKey,
|
|
safety_identifier: openaiOptions?.safetyIdentifier,
|
|
top_logprobs: topLogprobs,
|
|
|
|
// model-specific settings:
|
|
...(modelConfig.isReasoningModel &&
|
|
(openaiOptions?.reasoningEffort != null || openaiOptions?.reasoningSummary != null) && {
|
|
reasoning: {
|
|
...(openaiOptions?.reasoningEffort != null && {
|
|
effort: openaiOptions.reasoningEffort,
|
|
}),
|
|
...(openaiOptions?.reasoningSummary != null && {
|
|
summary: openaiOptions.reasoningSummary,
|
|
}),
|
|
},
|
|
}),
|
|
...(modelConfig.requiredAutoTruncation && {
|
|
truncation: "auto",
|
|
}),
|
|
}
|
|
|
|
if (modelConfig.isReasoningModel) {
|
|
// remove unsupported settings for reasoning models
|
|
// see https://platform.openai.com/docs/guides/reasoning#limitations
|
|
if (baseArgs.temperature != null) {
|
|
baseArgs.temperature = undefined
|
|
warnings.push({
|
|
type: "unsupported",
|
|
feature: "temperature",
|
|
details: "temperature is not supported for reasoning models",
|
|
})
|
|
}
|
|
|
|
if (baseArgs.top_p != null) {
|
|
baseArgs.top_p = undefined
|
|
warnings.push({
|
|
type: "unsupported",
|
|
feature: "topP",
|
|
details: "topP is not supported for reasoning models",
|
|
})
|
|
}
|
|
} else {
|
|
if (openaiOptions?.reasoningEffort != null) {
|
|
warnings.push({
|
|
type: "unsupported",
|
|
feature: "reasoningEffort",
|
|
details: "reasoningEffort is not supported for non-reasoning models",
|
|
})
|
|
}
|
|
|
|
if (openaiOptions?.reasoningSummary != null) {
|
|
warnings.push({
|
|
type: "unsupported",
|
|
feature: "reasoningSummary",
|
|
details: "reasoningSummary is not supported for non-reasoning models",
|
|
})
|
|
}
|
|
}
|
|
|
|
// Validate flex processing support
|
|
if (openaiOptions?.serviceTier === "flex" && !modelConfig.supportsFlexProcessing) {
|
|
warnings.push({
|
|
type: "unsupported",
|
|
feature: "serviceTier",
|
|
details: "flex processing is only available for o3, o4-mini, and gpt-5 models",
|
|
})
|
|
// Remove from args if not supported
|
|
baseArgs.service_tier = undefined
|
|
}
|
|
|
|
// Validate priority processing support
|
|
if (openaiOptions?.serviceTier === "priority" && !modelConfig.supportsPriorityProcessing) {
|
|
warnings.push({
|
|
type: "unsupported",
|
|
feature: "serviceTier",
|
|
details:
|
|
"priority processing is only available for supported models (gpt-4, gpt-5, gpt-5-mini, o3, o4-mini) and requires Enterprise access. gpt-5-nano is not supported",
|
|
})
|
|
// Remove from args if not supported
|
|
baseArgs.service_tier = undefined
|
|
}
|
|
|
|
const {
|
|
tools: openaiTools,
|
|
toolChoice: openaiToolChoice,
|
|
toolWarnings,
|
|
} = prepareResponsesTools({
|
|
tools,
|
|
toolChoice,
|
|
strictJsonSchema,
|
|
})
|
|
|
|
return {
|
|
webSearchToolName,
|
|
args: {
|
|
...baseArgs,
|
|
tools: openaiTools,
|
|
tool_choice: openaiToolChoice,
|
|
},
|
|
warnings: [...warnings, ...toolWarnings],
|
|
}
|
|
}
|
|
|
|
async doGenerate(options: LanguageModelV3CallOptions) {
|
|
const { args: body, warnings, webSearchToolName } = await this.getArgs(options)
|
|
const url = this.config.url({
|
|
path: "/responses",
|
|
modelId: this.modelId,
|
|
})
|
|
|
|
const {
|
|
responseHeaders,
|
|
value: response,
|
|
rawValue: rawResponse,
|
|
} = await postJsonToApi({
|
|
url,
|
|
headers: combineHeaders(this.config.headers(), options.headers),
|
|
body,
|
|
failedResponseHandler: openaiFailedResponseHandler,
|
|
successfulResponseHandler: createJsonResponseHandler(
|
|
z.object({
|
|
id: z.string(),
|
|
created_at: z.number(),
|
|
error: z
|
|
.object({
|
|
code: z.string(),
|
|
message: z.string(),
|
|
})
|
|
.nullish(),
|
|
model: z.string(),
|
|
output: z.array(
|
|
z.discriminatedUnion("type", [
|
|
z.object({
|
|
type: z.literal("message"),
|
|
role: z.literal("assistant"),
|
|
id: z.string(),
|
|
content: z.array(
|
|
z.object({
|
|
type: z.literal("output_text"),
|
|
text: z.string(),
|
|
logprobs: LOGPROBS_SCHEMA.nullish(),
|
|
annotations: z.array(
|
|
z.discriminatedUnion("type", [
|
|
z.object({
|
|
type: z.literal("url_citation"),
|
|
start_index: z.number(),
|
|
end_index: z.number(),
|
|
url: z.string(),
|
|
title: z.string(),
|
|
}),
|
|
z.object({
|
|
type: z.literal("file_citation"),
|
|
file_id: z.string(),
|
|
filename: z.string().nullish(),
|
|
index: z.number().nullish(),
|
|
start_index: z.number().nullish(),
|
|
end_index: z.number().nullish(),
|
|
quote: z.string().nullish(),
|
|
}),
|
|
z.object({
|
|
type: z.literal("container_file_citation"),
|
|
}),
|
|
]),
|
|
),
|
|
}),
|
|
),
|
|
}),
|
|
webSearchCallItem,
|
|
fileSearchCallItem,
|
|
codeInterpreterCallItem,
|
|
imageGenerationCallItem,
|
|
localShellCallItem,
|
|
z.object({
|
|
type: z.literal("function_call"),
|
|
call_id: z.string(),
|
|
name: z.string(),
|
|
arguments: z.string(),
|
|
id: z.string(),
|
|
}),
|
|
z.object({
|
|
type: z.literal("computer_call"),
|
|
id: z.string(),
|
|
status: z.string().optional(),
|
|
}),
|
|
z.object({
|
|
type: z.literal("reasoning"),
|
|
id: z.string(),
|
|
encrypted_content: z.string().nullish(),
|
|
summary: z.array(
|
|
z.object({
|
|
type: z.literal("summary_text"),
|
|
text: z.string(),
|
|
}),
|
|
),
|
|
}),
|
|
]),
|
|
),
|
|
service_tier: z.string().nullish(),
|
|
incomplete_details: z.object({ reason: z.string() }).nullish(),
|
|
usage: usageSchema,
|
|
}),
|
|
),
|
|
abortSignal: options.abortSignal,
|
|
fetch: this.config.fetch,
|
|
})
|
|
|
|
if (response.error) {
|
|
throw new APICallError({
|
|
message: response.error.message,
|
|
url,
|
|
requestBodyValues: body,
|
|
statusCode: 400,
|
|
responseHeaders,
|
|
responseBody: rawResponse as string,
|
|
isRetryable: false,
|
|
})
|
|
}
|
|
|
|
const content: Array<LanguageModelV3Content> = []
|
|
const logprobs: Array<z.infer<typeof LOGPROBS_SCHEMA>> = []
|
|
|
|
// flag that checks if there have been client-side tool calls (not executed by openai)
|
|
let hasFunctionCall = false
|
|
|
|
// map response content to content array
|
|
for (const part of response.output) {
|
|
switch (part.type) {
|
|
case "reasoning": {
|
|
// when there are no summary parts, we need to add an empty reasoning part:
|
|
if (part.summary.length === 0) {
|
|
part.summary.push({ type: "summary_text", text: "" })
|
|
}
|
|
|
|
for (const summary of part.summary) {
|
|
content.push({
|
|
type: "reasoning" as const,
|
|
text: summary.text,
|
|
providerMetadata: {
|
|
openai: {
|
|
itemId: part.id,
|
|
reasoningEncryptedContent: part.encrypted_content ?? null,
|
|
},
|
|
},
|
|
})
|
|
}
|
|
break
|
|
}
|
|
|
|
case "image_generation_call": {
|
|
content.push({
|
|
type: "tool-call",
|
|
toolCallId: part.id,
|
|
toolName: "image_generation",
|
|
input: "{}",
|
|
providerExecuted: true,
|
|
})
|
|
|
|
content.push({
|
|
type: "tool-result",
|
|
toolCallId: part.id,
|
|
toolName: "image_generation",
|
|
result: {
|
|
result: part.result,
|
|
} satisfies z.infer<typeof imageGenerationOutputSchema>,
|
|
})
|
|
|
|
break
|
|
}
|
|
|
|
case "local_shell_call": {
|
|
content.push({
|
|
type: "tool-call",
|
|
toolCallId: part.call_id,
|
|
toolName: "local_shell",
|
|
input: JSON.stringify({ action: part.action } satisfies z.infer<typeof localShellInputSchema>),
|
|
providerMetadata: {
|
|
openai: {
|
|
itemId: part.id,
|
|
},
|
|
},
|
|
})
|
|
|
|
break
|
|
}
|
|
|
|
case "message": {
|
|
for (const contentPart of part.content) {
|
|
if (options.providerOptions?.openai?.logprobs && contentPart.logprobs) {
|
|
logprobs.push(contentPart.logprobs)
|
|
}
|
|
|
|
content.push({
|
|
type: "text",
|
|
text: contentPart.text,
|
|
providerMetadata: {
|
|
openai: {
|
|
itemId: part.id,
|
|
},
|
|
},
|
|
})
|
|
|
|
for (const annotation of contentPart.annotations) {
|
|
if (annotation.type === "url_citation") {
|
|
content.push({
|
|
type: "source",
|
|
sourceType: "url",
|
|
id: this.config.generateId?.() ?? generateId(),
|
|
url: annotation.url,
|
|
title: annotation.title,
|
|
})
|
|
} else if (annotation.type === "file_citation") {
|
|
content.push({
|
|
type: "source",
|
|
sourceType: "document",
|
|
id: this.config.generateId?.() ?? generateId(),
|
|
mediaType: "text/plain",
|
|
title: annotation.quote ?? annotation.filename ?? "Document",
|
|
filename: annotation.filename ?? annotation.file_id,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
break
|
|
}
|
|
|
|
case "function_call": {
|
|
hasFunctionCall = true
|
|
|
|
content.push({
|
|
type: "tool-call",
|
|
toolCallId: part.call_id,
|
|
toolName: part.name,
|
|
input: part.arguments,
|
|
providerMetadata: {
|
|
openai: {
|
|
itemId: part.id,
|
|
},
|
|
},
|
|
})
|
|
break
|
|
}
|
|
|
|
case "web_search_call": {
|
|
content.push({
|
|
type: "tool-call",
|
|
toolCallId: part.id,
|
|
toolName: webSearchToolName ?? "web_search",
|
|
input: JSON.stringify({ action: part.action }),
|
|
providerExecuted: true,
|
|
})
|
|
|
|
content.push({
|
|
type: "tool-result",
|
|
toolCallId: part.id,
|
|
toolName: webSearchToolName ?? "web_search",
|
|
result: { status: part.status },
|
|
})
|
|
|
|
break
|
|
}
|
|
|
|
case "computer_call": {
|
|
content.push({
|
|
type: "tool-call",
|
|
toolCallId: part.id,
|
|
toolName: "computer_use",
|
|
input: "",
|
|
providerExecuted: true,
|
|
})
|
|
|
|
content.push({
|
|
type: "tool-result",
|
|
toolCallId: part.id,
|
|
toolName: "computer_use",
|
|
result: {
|
|
type: "computer_use_tool_result",
|
|
status: part.status || "completed",
|
|
},
|
|
})
|
|
break
|
|
}
|
|
|
|
case "file_search_call": {
|
|
content.push({
|
|
type: "tool-call",
|
|
toolCallId: part.id,
|
|
toolName: "file_search",
|
|
input: "{}",
|
|
providerExecuted: true,
|
|
})
|
|
|
|
content.push({
|
|
type: "tool-result",
|
|
toolCallId: part.id,
|
|
toolName: "file_search",
|
|
result: {
|
|
queries: part.queries,
|
|
results:
|
|
part.results?.map((result) => ({
|
|
attributes: result.attributes as Record<string, JSONValue>,
|
|
fileId: result.file_id,
|
|
filename: result.filename,
|
|
score: result.score,
|
|
text: result.text,
|
|
})) ?? null,
|
|
} satisfies z.infer<typeof fileSearchOutputSchema>,
|
|
})
|
|
break
|
|
}
|
|
|
|
case "code_interpreter_call": {
|
|
content.push({
|
|
type: "tool-call",
|
|
toolCallId: part.id,
|
|
toolName: "code_interpreter",
|
|
input: JSON.stringify({
|
|
code: part.code,
|
|
containerId: part.container_id,
|
|
} satisfies z.infer<typeof codeInterpreterInputSchema>),
|
|
providerExecuted: true,
|
|
})
|
|
|
|
content.push({
|
|
type: "tool-result",
|
|
toolCallId: part.id,
|
|
toolName: "code_interpreter",
|
|
result: {
|
|
outputs: part.outputs,
|
|
} satisfies z.infer<typeof codeInterpreterOutputSchema>,
|
|
})
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
const providerMetadata: SharedV3ProviderMetadata = {
|
|
openai: { responseId: response.id },
|
|
}
|
|
|
|
if (logprobs.length > 0) {
|
|
providerMetadata.openai.logprobs = logprobs
|
|
}
|
|
|
|
if (typeof response.service_tier === "string") {
|
|
providerMetadata.openai.serviceTier = response.service_tier
|
|
}
|
|
|
|
return {
|
|
content,
|
|
finishReason: {
|
|
unified: mapOpenAIResponseFinishReason({
|
|
finishReason: response.incomplete_details?.reason,
|
|
hasFunctionCall,
|
|
}),
|
|
raw: response.incomplete_details?.reason,
|
|
},
|
|
usage: {
|
|
inputTokens: {
|
|
total: response.usage.input_tokens,
|
|
noCache:
|
|
response.usage.input_tokens_details?.cached_tokens != null
|
|
? response.usage.input_tokens - response.usage.input_tokens_details.cached_tokens
|
|
: undefined,
|
|
cacheRead: response.usage.input_tokens_details?.cached_tokens ?? undefined,
|
|
cacheWrite: undefined,
|
|
},
|
|
outputTokens: {
|
|
total: response.usage.output_tokens,
|
|
text: undefined,
|
|
reasoning: response.usage.output_tokens_details?.reasoning_tokens ?? undefined,
|
|
},
|
|
raw: response.usage,
|
|
},
|
|
request: { body },
|
|
response: {
|
|
id: response.id,
|
|
timestamp: new Date(response.created_at * 1000),
|
|
modelId: response.model,
|
|
headers: responseHeaders,
|
|
body: rawResponse,
|
|
},
|
|
providerMetadata,
|
|
warnings,
|
|
}
|
|
}
|
|
|
|
async doStream(options: LanguageModelV3CallOptions) {
|
|
const { args: body, warnings, webSearchToolName } = await this.getArgs(options)
|
|
|
|
const { responseHeaders, value: response } = await postJsonToApi({
|
|
url: this.config.url({
|
|
path: "/responses",
|
|
modelId: this.modelId,
|
|
}),
|
|
headers: combineHeaders(this.config.headers(), options.headers),
|
|
body: {
|
|
...body,
|
|
stream: true,
|
|
},
|
|
failedResponseHandler: openaiFailedResponseHandler,
|
|
successfulResponseHandler: createEventSourceResponseHandler(openaiResponsesChunkSchema),
|
|
abortSignal: options.abortSignal,
|
|
fetch: this.config.fetch,
|
|
})
|
|
|
|
// oxlint-disable-next-line no-this-alias -- needed for closure scope inside generator
|
|
const self = this
|
|
|
|
let finishReason: {
|
|
unified: ReturnType<typeof mapOpenAIResponseFinishReason>
|
|
raw: string | undefined
|
|
} = {
|
|
unified: "other",
|
|
raw: undefined,
|
|
}
|
|
const usage: {
|
|
inputTokens: number | undefined
|
|
outputTokens: number | undefined
|
|
totalTokens: number | undefined
|
|
reasoningTokens: number | undefined
|
|
cachedInputTokens: number | undefined
|
|
} = {
|
|
inputTokens: undefined,
|
|
outputTokens: undefined,
|
|
totalTokens: undefined,
|
|
reasoningTokens: undefined,
|
|
cachedInputTokens: undefined,
|
|
}
|
|
const logprobs: Array<z.infer<typeof LOGPROBS_SCHEMA>> = []
|
|
let responseId: string | null = null
|
|
const ongoingToolCalls: Record<
|
|
number,
|
|
| {
|
|
toolName: string
|
|
toolCallId: string
|
|
codeInterpreter?: {
|
|
containerId: string
|
|
}
|
|
}
|
|
| undefined
|
|
> = {}
|
|
|
|
// flag that checks if there have been client-side tool calls (not executed by openai)
|
|
let hasFunctionCall = false
|
|
|
|
// Track reasoning by output_index instead of item_id
|
|
// GitHub Copilot rotates encrypted item IDs on every event
|
|
const activeReasoning: Record<
|
|
number,
|
|
{
|
|
canonicalId: string // the item.id from output_item.added
|
|
encryptedContent?: string | null
|
|
summaryParts: number[]
|
|
}
|
|
> = {}
|
|
|
|
// Track current active reasoning output_index for correlating summary events
|
|
let currentReasoningOutputIndex: number | null = null
|
|
|
|
// Track a stable text part id for the current assistant message.
|
|
// Copilot may change item_id across text deltas; normalize to one id.
|
|
let currentTextId: string | null = null
|
|
|
|
let serviceTier: string | undefined
|
|
|
|
return {
|
|
stream: response.pipeThrough(
|
|
new TransformStream<ParseResult<z.infer<typeof openaiResponsesChunkSchema>>, LanguageModelV3StreamPart>({
|
|
start(controller) {
|
|
controller.enqueue({ type: "stream-start", warnings })
|
|
},
|
|
|
|
transform(chunk, controller) {
|
|
if (options.includeRawChunks) {
|
|
controller.enqueue({ type: "raw", rawValue: chunk.rawValue })
|
|
}
|
|
|
|
// handle failed chunk parsing / validation:
|
|
if (!chunk.success) {
|
|
finishReason = {
|
|
unified: "error",
|
|
raw: undefined,
|
|
}
|
|
controller.enqueue({ type: "error", error: chunk.error })
|
|
return
|
|
}
|
|
|
|
const value = chunk.value
|
|
|
|
if (isResponseOutputItemAddedChunk(value)) {
|
|
if (value.item.type === "function_call") {
|
|
ongoingToolCalls[value.output_index] = {
|
|
toolName: value.item.name,
|
|
toolCallId: value.item.call_id,
|
|
}
|
|
|
|
controller.enqueue({
|
|
type: "tool-input-start",
|
|
id: value.item.call_id,
|
|
toolName: value.item.name,
|
|
})
|
|
} else if (value.item.type === "web_search_call") {
|
|
ongoingToolCalls[value.output_index] = {
|
|
toolName: webSearchToolName ?? "web_search",
|
|
toolCallId: value.item.id,
|
|
}
|
|
|
|
controller.enqueue({
|
|
type: "tool-input-start",
|
|
id: value.item.id,
|
|
toolName: webSearchToolName ?? "web_search",
|
|
})
|
|
} else if (value.item.type === "computer_call") {
|
|
ongoingToolCalls[value.output_index] = {
|
|
toolName: "computer_use",
|
|
toolCallId: value.item.id,
|
|
}
|
|
|
|
controller.enqueue({
|
|
type: "tool-input-start",
|
|
id: value.item.id,
|
|
toolName: "computer_use",
|
|
})
|
|
} else if (value.item.type === "code_interpreter_call") {
|
|
ongoingToolCalls[value.output_index] = {
|
|
toolName: "code_interpreter",
|
|
toolCallId: value.item.id,
|
|
codeInterpreter: {
|
|
containerId: value.item.container_id,
|
|
},
|
|
}
|
|
|
|
controller.enqueue({
|
|
type: "tool-input-start",
|
|
id: value.item.id,
|
|
toolName: "code_interpreter",
|
|
})
|
|
|
|
controller.enqueue({
|
|
type: "tool-input-delta",
|
|
id: value.item.id,
|
|
delta: `{"containerId":"${value.item.container_id}","code":"`,
|
|
})
|
|
} else if (value.item.type === "file_search_call") {
|
|
controller.enqueue({
|
|
type: "tool-call",
|
|
toolCallId: value.item.id,
|
|
toolName: "file_search",
|
|
input: "{}",
|
|
providerExecuted: true,
|
|
})
|
|
} else if (value.item.type === "image_generation_call") {
|
|
controller.enqueue({
|
|
type: "tool-call",
|
|
toolCallId: value.item.id,
|
|
toolName: "image_generation",
|
|
input: "{}",
|
|
providerExecuted: true,
|
|
})
|
|
} else if (value.item.type === "message") {
|
|
// Start a stable text part for this assistant message
|
|
currentTextId = value.item.id
|
|
controller.enqueue({
|
|
type: "text-start",
|
|
id: value.item.id,
|
|
providerMetadata: {
|
|
openai: {
|
|
itemId: value.item.id,
|
|
},
|
|
},
|
|
})
|
|
} else if (isResponseOutputItemAddedReasoningChunk(value)) {
|
|
activeReasoning[value.output_index] = {
|
|
canonicalId: value.item.id,
|
|
encryptedContent: value.item.encrypted_content,
|
|
summaryParts: [0],
|
|
}
|
|
currentReasoningOutputIndex = value.output_index
|
|
|
|
controller.enqueue({
|
|
type: "reasoning-start",
|
|
id: `${value.item.id}:0`,
|
|
providerMetadata: {
|
|
openai: {
|
|
itemId: value.item.id,
|
|
reasoningEncryptedContent: value.item.encrypted_content ?? null,
|
|
},
|
|
},
|
|
})
|
|
}
|
|
} else if (isResponseOutputItemDoneChunk(value)) {
|
|
if (value.item.type === "function_call") {
|
|
ongoingToolCalls[value.output_index] = undefined
|
|
hasFunctionCall = true
|
|
|
|
controller.enqueue({
|
|
type: "tool-input-end",
|
|
id: value.item.call_id,
|
|
})
|
|
|
|
controller.enqueue({
|
|
type: "tool-call",
|
|
toolCallId: value.item.call_id,
|
|
toolName: value.item.name,
|
|
input: value.item.arguments,
|
|
providerMetadata: {
|
|
openai: {
|
|
itemId: value.item.id,
|
|
},
|
|
},
|
|
})
|
|
} else if (value.item.type === "web_search_call") {
|
|
ongoingToolCalls[value.output_index] = undefined
|
|
|
|
controller.enqueue({
|
|
type: "tool-input-end",
|
|
id: value.item.id,
|
|
})
|
|
|
|
controller.enqueue({
|
|
type: "tool-call",
|
|
toolCallId: value.item.id,
|
|
toolName: "web_search",
|
|
input: JSON.stringify({ action: value.item.action }),
|
|
providerExecuted: true,
|
|
})
|
|
|
|
controller.enqueue({
|
|
type: "tool-result",
|
|
toolCallId: value.item.id,
|
|
toolName: "web_search",
|
|
result: { status: value.item.status },
|
|
})
|
|
} else if (value.item.type === "computer_call") {
|
|
ongoingToolCalls[value.output_index] = undefined
|
|
|
|
controller.enqueue({
|
|
type: "tool-input-end",
|
|
id: value.item.id,
|
|
})
|
|
|
|
controller.enqueue({
|
|
type: "tool-call",
|
|
toolCallId: value.item.id,
|
|
toolName: "computer_use",
|
|
input: "",
|
|
providerExecuted: true,
|
|
})
|
|
|
|
controller.enqueue({
|
|
type: "tool-result",
|
|
toolCallId: value.item.id,
|
|
toolName: "computer_use",
|
|
result: {
|
|
type: "computer_use_tool_result",
|
|
status: value.item.status || "completed",
|
|
},
|
|
})
|
|
} else if (value.item.type === "file_search_call") {
|
|
ongoingToolCalls[value.output_index] = undefined
|
|
|
|
controller.enqueue({
|
|
type: "tool-result",
|
|
toolCallId: value.item.id,
|
|
toolName: "file_search",
|
|
result: {
|
|
queries: value.item.queries,
|
|
results:
|
|
value.item.results?.map((result) => ({
|
|
attributes: result.attributes as Record<string, JSONValue>,
|
|
fileId: result.file_id,
|
|
filename: result.filename,
|
|
score: result.score,
|
|
text: result.text,
|
|
})) ?? null,
|
|
} satisfies z.infer<typeof fileSearchOutputSchema>,
|
|
})
|
|
} else if (value.item.type === "code_interpreter_call") {
|
|
ongoingToolCalls[value.output_index] = undefined
|
|
|
|
controller.enqueue({
|
|
type: "tool-result",
|
|
toolCallId: value.item.id,
|
|
toolName: "code_interpreter",
|
|
result: {
|
|
outputs: value.item.outputs,
|
|
} satisfies z.infer<typeof codeInterpreterOutputSchema>,
|
|
})
|
|
} else if (value.item.type === "image_generation_call") {
|
|
controller.enqueue({
|
|
type: "tool-result",
|
|
toolCallId: value.item.id,
|
|
toolName: "image_generation",
|
|
result: {
|
|
result: value.item.result,
|
|
} satisfies z.infer<typeof imageGenerationOutputSchema>,
|
|
})
|
|
} else if (value.item.type === "local_shell_call") {
|
|
ongoingToolCalls[value.output_index] = undefined
|
|
|
|
controller.enqueue({
|
|
type: "tool-call",
|
|
toolCallId: value.item.call_id,
|
|
toolName: "local_shell",
|
|
input: JSON.stringify({
|
|
action: {
|
|
type: "exec",
|
|
command: value.item.action.command,
|
|
timeoutMs: value.item.action.timeout_ms,
|
|
user: value.item.action.user,
|
|
workingDirectory: value.item.action.working_directory,
|
|
env: value.item.action.env,
|
|
},
|
|
} satisfies z.infer<typeof localShellInputSchema>),
|
|
providerMetadata: {
|
|
openai: { itemId: value.item.id },
|
|
},
|
|
})
|
|
} else if (value.item.type === "message") {
|
|
if (currentTextId) {
|
|
controller.enqueue({
|
|
type: "text-end",
|
|
id: currentTextId,
|
|
})
|
|
currentTextId = null
|
|
}
|
|
} else if (isResponseOutputItemDoneReasoningChunk(value)) {
|
|
const activeReasoningPart = activeReasoning[value.output_index]
|
|
if (activeReasoningPart) {
|
|
for (const summaryIndex of activeReasoningPart.summaryParts) {
|
|
controller.enqueue({
|
|
type: "reasoning-end",
|
|
id: `${activeReasoningPart.canonicalId}:${summaryIndex}`,
|
|
providerMetadata: {
|
|
openai: {
|
|
itemId: activeReasoningPart.canonicalId,
|
|
reasoningEncryptedContent: value.item.encrypted_content ?? null,
|
|
},
|
|
},
|
|
})
|
|
}
|
|
delete activeReasoning[value.output_index]
|
|
if (currentReasoningOutputIndex === value.output_index) {
|
|
currentReasoningOutputIndex = null
|
|
}
|
|
}
|
|
}
|
|
} else if (isResponseFunctionCallArgumentsDeltaChunk(value)) {
|
|
const toolCall = ongoingToolCalls[value.output_index]
|
|
|
|
if (toolCall != null) {
|
|
controller.enqueue({
|
|
type: "tool-input-delta",
|
|
id: toolCall.toolCallId,
|
|
delta: value.delta,
|
|
})
|
|
}
|
|
} else if (isResponseImageGenerationCallPartialImageChunk(value)) {
|
|
controller.enqueue({
|
|
type: "tool-result",
|
|
toolCallId: value.item_id,
|
|
toolName: "image_generation",
|
|
result: {
|
|
result: value.partial_image_b64,
|
|
} satisfies z.infer<typeof imageGenerationOutputSchema>,
|
|
})
|
|
} else if (isResponseCodeInterpreterCallCodeDeltaChunk(value)) {
|
|
const toolCall = ongoingToolCalls[value.output_index]
|
|
|
|
if (toolCall != null) {
|
|
controller.enqueue({
|
|
type: "tool-input-delta",
|
|
id: toolCall.toolCallId,
|
|
// The delta is code, which is embedding in a JSON string.
|
|
// To escape it, we use JSON.stringify and slice to remove the outer quotes.
|
|
delta: JSON.stringify(value.delta).slice(1, -1),
|
|
})
|
|
}
|
|
} else if (isResponseCodeInterpreterCallCodeDoneChunk(value)) {
|
|
const toolCall = ongoingToolCalls[value.output_index]
|
|
|
|
if (toolCall != null) {
|
|
controller.enqueue({
|
|
type: "tool-input-delta",
|
|
id: toolCall.toolCallId,
|
|
delta: '"}',
|
|
})
|
|
|
|
controller.enqueue({
|
|
type: "tool-input-end",
|
|
id: toolCall.toolCallId,
|
|
})
|
|
|
|
// immediately send the tool call after the input end:
|
|
controller.enqueue({
|
|
type: "tool-call",
|
|
toolCallId: toolCall.toolCallId,
|
|
toolName: "code_interpreter",
|
|
input: JSON.stringify({
|
|
code: value.code,
|
|
containerId: toolCall.codeInterpreter!.containerId,
|
|
} satisfies z.infer<typeof codeInterpreterInputSchema>),
|
|
providerExecuted: true,
|
|
})
|
|
}
|
|
} else if (isResponseCreatedChunk(value)) {
|
|
responseId = value.response.id
|
|
controller.enqueue({
|
|
type: "response-metadata",
|
|
id: value.response.id,
|
|
timestamp: new Date(value.response.created_at * 1000),
|
|
modelId: value.response.model,
|
|
})
|
|
} else if (isTextDeltaChunk(value)) {
|
|
// Ensure a text-start exists, and normalize deltas to a stable id
|
|
if (!currentTextId) {
|
|
currentTextId = value.item_id
|
|
controller.enqueue({
|
|
type: "text-start",
|
|
id: currentTextId,
|
|
providerMetadata: {
|
|
openai: { itemId: value.item_id },
|
|
},
|
|
})
|
|
}
|
|
|
|
controller.enqueue({
|
|
type: "text-delta",
|
|
id: currentTextId,
|
|
delta: value.delta,
|
|
})
|
|
|
|
if (options.providerOptions?.openai?.logprobs && value.logprobs) {
|
|
logprobs.push(value.logprobs)
|
|
}
|
|
} else if (isResponseReasoningSummaryPartAddedChunk(value)) {
|
|
const activeItem =
|
|
currentReasoningOutputIndex !== null ? activeReasoning[currentReasoningOutputIndex] : null
|
|
|
|
// the first reasoning start is pushed in isResponseOutputItemAddedReasoningChunk.
|
|
if (activeItem && value.summary_index > 0) {
|
|
activeItem.summaryParts.push(value.summary_index)
|
|
|
|
controller.enqueue({
|
|
type: "reasoning-start",
|
|
id: `${activeItem.canonicalId}:${value.summary_index}`,
|
|
providerMetadata: {
|
|
openai: {
|
|
itemId: activeItem.canonicalId,
|
|
reasoningEncryptedContent: activeItem.encryptedContent ?? null,
|
|
},
|
|
},
|
|
})
|
|
}
|
|
} else if (isResponseReasoningSummaryTextDeltaChunk(value)) {
|
|
const activeItem =
|
|
currentReasoningOutputIndex !== null ? activeReasoning[currentReasoningOutputIndex] : null
|
|
|
|
if (activeItem) {
|
|
controller.enqueue({
|
|
type: "reasoning-delta",
|
|
id: `${activeItem.canonicalId}:${value.summary_index}`,
|
|
delta: value.delta,
|
|
providerMetadata: {
|
|
openai: {
|
|
itemId: activeItem.canonicalId,
|
|
},
|
|
},
|
|
})
|
|
}
|
|
} else if (isResponseFinishedChunk(value)) {
|
|
finishReason = {
|
|
unified: mapOpenAIResponseFinishReason({
|
|
finishReason: value.response.incomplete_details?.reason,
|
|
hasFunctionCall,
|
|
}),
|
|
raw: value.response.incomplete_details?.reason ?? undefined,
|
|
}
|
|
usage.inputTokens = value.response.usage.input_tokens
|
|
usage.outputTokens = value.response.usage.output_tokens
|
|
usage.totalTokens = value.response.usage.input_tokens + value.response.usage.output_tokens
|
|
usage.reasoningTokens = value.response.usage.output_tokens_details?.reasoning_tokens ?? undefined
|
|
usage.cachedInputTokens = value.response.usage.input_tokens_details?.cached_tokens ?? undefined
|
|
if (typeof value.response.service_tier === "string") {
|
|
serviceTier = value.response.service_tier
|
|
}
|
|
} else if (isResponseAnnotationAddedChunk(value)) {
|
|
if (value.annotation.type === "url_citation") {
|
|
controller.enqueue({
|
|
type: "source",
|
|
sourceType: "url",
|
|
id: self.config.generateId?.() ?? generateId(),
|
|
url: value.annotation.url,
|
|
title: value.annotation.title,
|
|
})
|
|
} else if (value.annotation.type === "file_citation") {
|
|
controller.enqueue({
|
|
type: "source",
|
|
sourceType: "document",
|
|
id: self.config.generateId?.() ?? generateId(),
|
|
mediaType: "text/plain",
|
|
title: value.annotation.quote ?? value.annotation.filename ?? "Document",
|
|
filename: value.annotation.filename ?? value.annotation.file_id,
|
|
})
|
|
}
|
|
} else if (isErrorChunk(value)) {
|
|
controller.enqueue({ type: "error", error: value })
|
|
}
|
|
},
|
|
|
|
flush(controller) {
|
|
// Close any dangling text part
|
|
if (currentTextId) {
|
|
controller.enqueue({ type: "text-end", id: currentTextId })
|
|
currentTextId = null
|
|
}
|
|
|
|
const providerMetadata: SharedV3ProviderMetadata = {
|
|
openai: {
|
|
responseId,
|
|
},
|
|
}
|
|
|
|
if (logprobs.length > 0) {
|
|
providerMetadata.openai.logprobs = logprobs
|
|
}
|
|
|
|
if (serviceTier !== undefined) {
|
|
providerMetadata.openai.serviceTier = serviceTier
|
|
}
|
|
|
|
controller.enqueue({
|
|
type: "finish",
|
|
finishReason,
|
|
usage: {
|
|
inputTokens: {
|
|
total: usage.inputTokens,
|
|
noCache:
|
|
usage.inputTokens != null && usage.cachedInputTokens != null
|
|
? usage.inputTokens - usage.cachedInputTokens
|
|
: undefined,
|
|
cacheRead: usage.cachedInputTokens,
|
|
cacheWrite: undefined,
|
|
},
|
|
outputTokens: {
|
|
total: usage.outputTokens,
|
|
text: undefined,
|
|
reasoning: usage.reasoningTokens,
|
|
},
|
|
raw: {
|
|
input_tokens: usage.inputTokens,
|
|
output_tokens: usage.outputTokens,
|
|
total_tokens: usage.totalTokens,
|
|
},
|
|
},
|
|
providerMetadata,
|
|
})
|
|
},
|
|
}),
|
|
),
|
|
request: { body },
|
|
response: { headers: responseHeaders },
|
|
}
|
|
}
|
|
}
|
|
|
|
const usageSchema = z.object({
|
|
input_tokens: z.number(),
|
|
input_tokens_details: z.object({ cached_tokens: z.number().nullish() }).nullish(),
|
|
output_tokens: z.number(),
|
|
output_tokens_details: z.object({ reasoning_tokens: z.number().nullish() }).nullish(),
|
|
})
|
|
|
|
const textDeltaChunkSchema = z.object({
|
|
type: z.literal("response.output_text.delta"),
|
|
item_id: z.string(),
|
|
delta: z.string(),
|
|
logprobs: LOGPROBS_SCHEMA.nullish(),
|
|
})
|
|
|
|
const errorChunkSchema = z.object({
|
|
type: z.literal("error"),
|
|
code: z.string(),
|
|
message: z.string(),
|
|
param: z.string().nullish(),
|
|
sequence_number: z.number(),
|
|
})
|
|
|
|
const responseFinishedChunkSchema = z.object({
|
|
type: z.enum(["response.completed", "response.incomplete"]),
|
|
response: z.object({
|
|
incomplete_details: z.object({ reason: z.string() }).nullish(),
|
|
usage: usageSchema,
|
|
service_tier: z.string().nullish(),
|
|
}),
|
|
})
|
|
|
|
const responseCreatedChunkSchema = z.object({
|
|
type: z.literal("response.created"),
|
|
response: z.object({
|
|
id: z.string(),
|
|
created_at: z.number(),
|
|
model: z.string(),
|
|
service_tier: z.string().nullish(),
|
|
}),
|
|
})
|
|
|
|
const responseOutputItemAddedSchema = z.object({
|
|
type: z.literal("response.output_item.added"),
|
|
output_index: z.number(),
|
|
item: z.discriminatedUnion("type", [
|
|
z.object({
|
|
type: z.literal("message"),
|
|
id: z.string(),
|
|
}),
|
|
z.object({
|
|
type: z.literal("reasoning"),
|
|
id: z.string(),
|
|
encrypted_content: z.string().nullish(),
|
|
}),
|
|
z.object({
|
|
type: z.literal("function_call"),
|
|
id: z.string(),
|
|
call_id: z.string(),
|
|
name: z.string(),
|
|
arguments: z.string(),
|
|
}),
|
|
z.object({
|
|
type: z.literal("web_search_call"),
|
|
id: z.string(),
|
|
status: z.string(),
|
|
action: z
|
|
.object({
|
|
type: z.literal("search"),
|
|
query: z.string().optional(),
|
|
})
|
|
.nullish(),
|
|
}),
|
|
z.object({
|
|
type: z.literal("computer_call"),
|
|
id: z.string(),
|
|
status: z.string(),
|
|
}),
|
|
z.object({
|
|
type: z.literal("file_search_call"),
|
|
id: z.string(),
|
|
}),
|
|
z.object({
|
|
type: z.literal("image_generation_call"),
|
|
id: z.string(),
|
|
}),
|
|
z.object({
|
|
type: z.literal("code_interpreter_call"),
|
|
id: z.string(),
|
|
container_id: z.string(),
|
|
code: z.string().nullable(),
|
|
outputs: z
|
|
.array(
|
|
z.discriminatedUnion("type", [
|
|
z.object({ type: z.literal("logs"), logs: z.string() }),
|
|
z.object({ type: z.literal("image"), url: z.string() }),
|
|
]),
|
|
)
|
|
.nullable(),
|
|
status: z.string(),
|
|
}),
|
|
]),
|
|
})
|
|
|
|
const responseOutputItemDoneSchema = z.object({
|
|
type: z.literal("response.output_item.done"),
|
|
output_index: z.number(),
|
|
item: z.discriminatedUnion("type", [
|
|
z.object({
|
|
type: z.literal("message"),
|
|
id: z.string(),
|
|
}),
|
|
z.object({
|
|
type: z.literal("reasoning"),
|
|
id: z.string(),
|
|
encrypted_content: z.string().nullish(),
|
|
}),
|
|
z.object({
|
|
type: z.literal("function_call"),
|
|
id: z.string(),
|
|
call_id: z.string(),
|
|
name: z.string(),
|
|
arguments: z.string(),
|
|
status: z.literal("completed"),
|
|
}),
|
|
codeInterpreterCallItem,
|
|
imageGenerationCallItem,
|
|
webSearchCallItem,
|
|
fileSearchCallItem,
|
|
localShellCallItem,
|
|
z.object({
|
|
type: z.literal("computer_call"),
|
|
id: z.string(),
|
|
status: z.literal("completed"),
|
|
}),
|
|
]),
|
|
})
|
|
|
|
const responseFunctionCallArgumentsDeltaSchema = z.object({
|
|
type: z.literal("response.function_call_arguments.delta"),
|
|
item_id: z.string(),
|
|
output_index: z.number(),
|
|
delta: z.string(),
|
|
})
|
|
|
|
const responseImageGenerationCallPartialImageSchema = z.object({
|
|
type: z.literal("response.image_generation_call.partial_image"),
|
|
item_id: z.string(),
|
|
output_index: z.number(),
|
|
partial_image_b64: z.string(),
|
|
})
|
|
|
|
const responseCodeInterpreterCallCodeDeltaSchema = z.object({
|
|
type: z.literal("response.code_interpreter_call_code.delta"),
|
|
item_id: z.string(),
|
|
output_index: z.number(),
|
|
delta: z.string(),
|
|
})
|
|
|
|
const responseCodeInterpreterCallCodeDoneSchema = z.object({
|
|
type: z.literal("response.code_interpreter_call_code.done"),
|
|
item_id: z.string(),
|
|
output_index: z.number(),
|
|
code: z.string(),
|
|
})
|
|
|
|
const responseAnnotationAddedSchema = z.object({
|
|
type: z.literal("response.output_text.annotation.added"),
|
|
annotation: z.discriminatedUnion("type", [
|
|
z.object({
|
|
type: z.literal("url_citation"),
|
|
url: z.string(),
|
|
title: z.string(),
|
|
}),
|
|
z.object({
|
|
type: z.literal("file_citation"),
|
|
file_id: z.string(),
|
|
filename: z.string().nullish(),
|
|
index: z.number().nullish(),
|
|
start_index: z.number().nullish(),
|
|
end_index: z.number().nullish(),
|
|
quote: z.string().nullish(),
|
|
}),
|
|
]),
|
|
})
|
|
|
|
const responseReasoningSummaryPartAddedSchema = z.object({
|
|
type: z.literal("response.reasoning_summary_part.added"),
|
|
item_id: z.string(),
|
|
summary_index: z.number(),
|
|
})
|
|
|
|
const responseReasoningSummaryTextDeltaSchema = z.object({
|
|
type: z.literal("response.reasoning_summary_text.delta"),
|
|
item_id: z.string(),
|
|
summary_index: z.number(),
|
|
delta: z.string(),
|
|
})
|
|
|
|
const openaiResponsesChunkSchema = z.union([
|
|
textDeltaChunkSchema,
|
|
responseFinishedChunkSchema,
|
|
responseCreatedChunkSchema,
|
|
responseOutputItemAddedSchema,
|
|
responseOutputItemDoneSchema,
|
|
responseFunctionCallArgumentsDeltaSchema,
|
|
responseImageGenerationCallPartialImageSchema,
|
|
responseCodeInterpreterCallCodeDeltaSchema,
|
|
responseCodeInterpreterCallCodeDoneSchema,
|
|
responseAnnotationAddedSchema,
|
|
responseReasoningSummaryPartAddedSchema,
|
|
responseReasoningSummaryTextDeltaSchema,
|
|
errorChunkSchema,
|
|
z.object({ type: z.string() }).loose(), // fallback for unknown chunks
|
|
])
|
|
|
|
type ExtractByType<T, K extends T extends { type: infer U } ? U : never> = T extends { type: K } ? T : never
|
|
|
|
function isTextDeltaChunk(
|
|
chunk: z.infer<typeof openaiResponsesChunkSchema>,
|
|
): chunk is z.infer<typeof textDeltaChunkSchema> {
|
|
return chunk.type === "response.output_text.delta"
|
|
}
|
|
|
|
function isResponseOutputItemDoneChunk(
|
|
chunk: z.infer<typeof openaiResponsesChunkSchema>,
|
|
): chunk is z.infer<typeof responseOutputItemDoneSchema> {
|
|
return chunk.type === "response.output_item.done"
|
|
}
|
|
|
|
function isResponseOutputItemDoneReasoningChunk(chunk: z.infer<typeof openaiResponsesChunkSchema>): chunk is z.infer<
|
|
typeof responseOutputItemDoneSchema
|
|
> & {
|
|
item: ExtractByType<z.infer<typeof responseOutputItemDoneSchema>["item"], "reasoning">
|
|
} {
|
|
return isResponseOutputItemDoneChunk(chunk) && chunk.item.type === "reasoning"
|
|
}
|
|
|
|
function isResponseFinishedChunk(
|
|
chunk: z.infer<typeof openaiResponsesChunkSchema>,
|
|
): chunk is z.infer<typeof responseFinishedChunkSchema> {
|
|
return chunk.type === "response.completed" || chunk.type === "response.incomplete"
|
|
}
|
|
|
|
function isResponseCreatedChunk(
|
|
chunk: z.infer<typeof openaiResponsesChunkSchema>,
|
|
): chunk is z.infer<typeof responseCreatedChunkSchema> {
|
|
return chunk.type === "response.created"
|
|
}
|
|
|
|
function isResponseFunctionCallArgumentsDeltaChunk(
|
|
chunk: z.infer<typeof openaiResponsesChunkSchema>,
|
|
): chunk is z.infer<typeof responseFunctionCallArgumentsDeltaSchema> {
|
|
return chunk.type === "response.function_call_arguments.delta"
|
|
}
|
|
function isResponseImageGenerationCallPartialImageChunk(
|
|
chunk: z.infer<typeof openaiResponsesChunkSchema>,
|
|
): chunk is z.infer<typeof responseImageGenerationCallPartialImageSchema> {
|
|
return chunk.type === "response.image_generation_call.partial_image"
|
|
}
|
|
|
|
function isResponseCodeInterpreterCallCodeDeltaChunk(
|
|
chunk: z.infer<typeof openaiResponsesChunkSchema>,
|
|
): chunk is z.infer<typeof responseCodeInterpreterCallCodeDeltaSchema> {
|
|
return chunk.type === "response.code_interpreter_call_code.delta"
|
|
}
|
|
|
|
function isResponseCodeInterpreterCallCodeDoneChunk(
|
|
chunk: z.infer<typeof openaiResponsesChunkSchema>,
|
|
): chunk is z.infer<typeof responseCodeInterpreterCallCodeDoneSchema> {
|
|
return chunk.type === "response.code_interpreter_call_code.done"
|
|
}
|
|
|
|
function isResponseOutputItemAddedChunk(
|
|
chunk: z.infer<typeof openaiResponsesChunkSchema>,
|
|
): chunk is z.infer<typeof responseOutputItemAddedSchema> {
|
|
return chunk.type === "response.output_item.added"
|
|
}
|
|
|
|
function isResponseOutputItemAddedReasoningChunk(chunk: z.infer<typeof openaiResponsesChunkSchema>): chunk is z.infer<
|
|
typeof responseOutputItemAddedSchema
|
|
> & {
|
|
item: ExtractByType<z.infer<typeof responseOutputItemAddedSchema>["item"], "reasoning">
|
|
} {
|
|
return isResponseOutputItemAddedChunk(chunk) && chunk.item.type === "reasoning"
|
|
}
|
|
|
|
function isResponseAnnotationAddedChunk(
|
|
chunk: z.infer<typeof openaiResponsesChunkSchema>,
|
|
): chunk is z.infer<typeof responseAnnotationAddedSchema> {
|
|
return chunk.type === "response.output_text.annotation.added"
|
|
}
|
|
|
|
function isResponseReasoningSummaryPartAddedChunk(
|
|
chunk: z.infer<typeof openaiResponsesChunkSchema>,
|
|
): chunk is z.infer<typeof responseReasoningSummaryPartAddedSchema> {
|
|
return chunk.type === "response.reasoning_summary_part.added"
|
|
}
|
|
|
|
function isResponseReasoningSummaryTextDeltaChunk(
|
|
chunk: z.infer<typeof openaiResponsesChunkSchema>,
|
|
): chunk is z.infer<typeof responseReasoningSummaryTextDeltaSchema> {
|
|
return chunk.type === "response.reasoning_summary_text.delta"
|
|
}
|
|
|
|
function isErrorChunk(chunk: z.infer<typeof openaiResponsesChunkSchema>): chunk is z.infer<typeof errorChunkSchema> {
|
|
return chunk.type === "error"
|
|
}
|
|
|
|
type ResponsesModelConfig = {
|
|
isReasoningModel: boolean
|
|
systemMessageMode: "remove" | "system" | "developer"
|
|
requiredAutoTruncation: boolean
|
|
supportsFlexProcessing: boolean
|
|
supportsPriorityProcessing: boolean
|
|
}
|
|
|
|
function getResponsesModelConfig(modelId: string): ResponsesModelConfig {
|
|
const supportsFlexProcessing =
|
|
modelId.startsWith("o3") ||
|
|
modelId.startsWith("o4-mini") ||
|
|
(modelId.startsWith("gpt-5") && !modelId.startsWith("gpt-5-chat"))
|
|
const supportsPriorityProcessing =
|
|
modelId.startsWith("gpt-4") ||
|
|
modelId.startsWith("gpt-5-mini") ||
|
|
(modelId.startsWith("gpt-5") && !modelId.startsWith("gpt-5-nano") && !modelId.startsWith("gpt-5-chat")) ||
|
|
modelId.startsWith("o3") ||
|
|
modelId.startsWith("o4-mini")
|
|
const defaults = {
|
|
requiredAutoTruncation: false,
|
|
systemMessageMode: "system" as const,
|
|
supportsFlexProcessing,
|
|
supportsPriorityProcessing,
|
|
}
|
|
|
|
// gpt-5-chat models are non-reasoning
|
|
if (modelId.startsWith("gpt-5-chat")) {
|
|
return {
|
|
...defaults,
|
|
isReasoningModel: false,
|
|
}
|
|
}
|
|
|
|
// o series reasoning models:
|
|
if (
|
|
modelId.startsWith("o") ||
|
|
modelId.startsWith("gpt-5") ||
|
|
modelId.startsWith("codex-") ||
|
|
modelId.startsWith("computer-use")
|
|
) {
|
|
if (modelId.startsWith("o1-mini") || modelId.startsWith("o1-preview")) {
|
|
return {
|
|
...defaults,
|
|
isReasoningModel: true,
|
|
systemMessageMode: "remove",
|
|
}
|
|
}
|
|
|
|
return {
|
|
...defaults,
|
|
isReasoningModel: true,
|
|
systemMessageMode: "developer",
|
|
}
|
|
}
|
|
|
|
// gpt models:
|
|
return {
|
|
...defaults,
|
|
isReasoningModel: false,
|
|
}
|
|
}
|
|
|
|
// TODO AI SDK 6: use optional here instead of nullish
|
|
const openaiResponsesProviderOptionsSchema = z.object({
|
|
include: z
|
|
.array(z.enum(["reasoning.encrypted_content", "file_search_call.results", "message.output_text.logprobs"]))
|
|
.nullish(),
|
|
instructions: z.string().nullish(),
|
|
|
|
/**
|
|
* Return the log probabilities of the tokens.
|
|
*
|
|
* Setting to true will return the log probabilities of the tokens that
|
|
* were generated.
|
|
*
|
|
* Setting to a number will return the log probabilities of the top n
|
|
* tokens that were generated.
|
|
*
|
|
* @see https://platform.openai.com/docs/api-reference/responses/create
|
|
* @see https://cookbook.openai.com/examples/using_logprobs
|
|
*/
|
|
logprobs: z.union([z.boolean(), z.number().min(1).max(TOP_LOGPROBS_MAX)]).optional(),
|
|
|
|
/**
|
|
* The maximum number of total calls to built-in tools that can be processed in a response.
|
|
* This maximum number applies across all built-in tool calls, not per individual tool.
|
|
* Any further attempts to call a tool by the model will be ignored.
|
|
*/
|
|
maxToolCalls: z.number().nullish(),
|
|
|
|
metadata: z.any().nullish(),
|
|
parallelToolCalls: z.boolean().nullish(),
|
|
previousResponseId: z.string().nullish(),
|
|
promptCacheKey: z.string().nullish(),
|
|
reasoningEffort: z.string().nullish(),
|
|
reasoningSummary: z.string().nullish(),
|
|
safetyIdentifier: z.string().nullish(),
|
|
serviceTier: z.enum(["auto", "flex", "priority"]).nullish(),
|
|
store: z.boolean().nullish(),
|
|
strictJsonSchema: z.boolean().nullish(),
|
|
textVerbosity: z.enum(["low", "medium", "high"]).nullish(),
|
|
user: z.string().nullish(),
|
|
})
|
|
|
|
export type OpenAIResponsesProviderOptions = z.infer<typeof openaiResponsesProviderOptionsSchema>
|