|
@@ -7,7 +7,7 @@ import {
|
|
|
type LanguageModelV2FinishReason,
|
|
type LanguageModelV2FinishReason,
|
|
|
type LanguageModelV2StreamPart,
|
|
type LanguageModelV2StreamPart,
|
|
|
type SharedV2ProviderMetadata,
|
|
type SharedV2ProviderMetadata,
|
|
|
-} from '@ai-sdk/provider';
|
|
|
|
|
|
|
+} from "@ai-sdk/provider"
|
|
|
import {
|
|
import {
|
|
|
combineHeaders,
|
|
combineHeaders,
|
|
|
createEventSourceResponseHandler,
|
|
createEventSourceResponseHandler,
|
|
@@ -20,80 +20,68 @@ import {
|
|
|
type ParseResult,
|
|
type ParseResult,
|
|
|
postJsonToApi,
|
|
postJsonToApi,
|
|
|
type ResponseHandler,
|
|
type ResponseHandler,
|
|
|
-} from '@ai-sdk/provider-utils';
|
|
|
|
|
-import { z } from 'zod/v4';
|
|
|
|
|
-import { convertToOpenAICompatibleChatMessages } from './convert-to-openai-compatible-chat-messages';
|
|
|
|
|
-import { getResponseMetadata } from './get-response-metadata';
|
|
|
|
|
-import { mapOpenAICompatibleFinishReason } from './map-openai-compatible-finish-reason';
|
|
|
|
|
-import {
|
|
|
|
|
- type OpenAICompatibleChatModelId,
|
|
|
|
|
- openaiCompatibleProviderOptions,
|
|
|
|
|
-} from './openai-compatible-chat-options';
|
|
|
|
|
-import {
|
|
|
|
|
- defaultOpenAICompatibleErrorStructure,
|
|
|
|
|
- type ProviderErrorStructure,
|
|
|
|
|
-} from '../openai-compatible-error';
|
|
|
|
|
-import type { MetadataExtractor } from './openai-compatible-metadata-extractor';
|
|
|
|
|
-import { prepareTools } from './openai-compatible-prepare-tools';
|
|
|
|
|
|
|
+} from "@ai-sdk/provider-utils"
|
|
|
|
|
+import { z } from "zod/v4"
|
|
|
|
|
+import { convertToOpenAICompatibleChatMessages } from "./convert-to-openai-compatible-chat-messages"
|
|
|
|
|
+import { getResponseMetadata } from "./get-response-metadata"
|
|
|
|
|
+import { mapOpenAICompatibleFinishReason } from "./map-openai-compatible-finish-reason"
|
|
|
|
|
+import { type OpenAICompatibleChatModelId, openaiCompatibleProviderOptions } from "./openai-compatible-chat-options"
|
|
|
|
|
+import { defaultOpenAICompatibleErrorStructure, type ProviderErrorStructure } from "../openai-compatible-error"
|
|
|
|
|
+import type { MetadataExtractor } from "./openai-compatible-metadata-extractor"
|
|
|
|
|
+import { prepareTools } from "./openai-compatible-prepare-tools"
|
|
|
|
|
|
|
|
export type OpenAICompatibleChatConfig = {
|
|
export type OpenAICompatibleChatConfig = {
|
|
|
- provider: string;
|
|
|
|
|
- headers: () => Record<string, string | undefined>;
|
|
|
|
|
- url: (options: { modelId: string; path: string }) => string;
|
|
|
|
|
- fetch?: FetchFunction;
|
|
|
|
|
- includeUsage?: boolean;
|
|
|
|
|
- errorStructure?: ProviderErrorStructure<any>;
|
|
|
|
|
- metadataExtractor?: MetadataExtractor;
|
|
|
|
|
|
|
+ provider: string
|
|
|
|
|
+ headers: () => Record<string, string | undefined>
|
|
|
|
|
+ url: (options: { modelId: string; path: string }) => string
|
|
|
|
|
+ fetch?: FetchFunction
|
|
|
|
|
+ includeUsage?: boolean
|
|
|
|
|
+ errorStructure?: ProviderErrorStructure<any>
|
|
|
|
|
+ metadataExtractor?: MetadataExtractor
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
* Whether the model supports structured outputs.
|
|
* Whether the model supports structured outputs.
|
|
|
*/
|
|
*/
|
|
|
- supportsStructuredOutputs?: boolean;
|
|
|
|
|
|
|
+ supportsStructuredOutputs?: boolean
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
* The supported URLs for the model.
|
|
* The supported URLs for the model.
|
|
|
*/
|
|
*/
|
|
|
- supportedUrls?: () => LanguageModelV2['supportedUrls'];
|
|
|
|
|
-};
|
|
|
|
|
|
|
+ supportedUrls?: () => LanguageModelV2["supportedUrls"]
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
export class OpenAICompatibleChatLanguageModel implements LanguageModelV2 {
|
|
export class OpenAICompatibleChatLanguageModel implements LanguageModelV2 {
|
|
|
- readonly specificationVersion = 'v2';
|
|
|
|
|
|
|
+ readonly specificationVersion = "v2"
|
|
|
|
|
|
|
|
- readonly supportsStructuredOutputs: boolean;
|
|
|
|
|
|
|
+ readonly supportsStructuredOutputs: boolean
|
|
|
|
|
|
|
|
- readonly modelId: OpenAICompatibleChatModelId;
|
|
|
|
|
- private readonly config: OpenAICompatibleChatConfig;
|
|
|
|
|
- private readonly failedResponseHandler: ResponseHandler<APICallError>;
|
|
|
|
|
- private readonly chunkSchema; // type inferred via constructor
|
|
|
|
|
|
|
+ readonly modelId: OpenAICompatibleChatModelId
|
|
|
|
|
+ private readonly config: OpenAICompatibleChatConfig
|
|
|
|
|
+ private readonly failedResponseHandler: ResponseHandler<APICallError>
|
|
|
|
|
+ private readonly chunkSchema // type inferred via constructor
|
|
|
|
|
|
|
|
- constructor(
|
|
|
|
|
- modelId: OpenAICompatibleChatModelId,
|
|
|
|
|
- config: OpenAICompatibleChatConfig,
|
|
|
|
|
- ) {
|
|
|
|
|
- this.modelId = modelId;
|
|
|
|
|
- this.config = config;
|
|
|
|
|
|
|
+ constructor(modelId: OpenAICompatibleChatModelId, config: OpenAICompatibleChatConfig) {
|
|
|
|
|
+ this.modelId = modelId
|
|
|
|
|
+ this.config = config
|
|
|
|
|
|
|
|
// initialize error handling:
|
|
// initialize error handling:
|
|
|
- const errorStructure =
|
|
|
|
|
- config.errorStructure ?? defaultOpenAICompatibleErrorStructure;
|
|
|
|
|
- this.chunkSchema = createOpenAICompatibleChatChunkSchema(
|
|
|
|
|
- errorStructure.errorSchema,
|
|
|
|
|
- );
|
|
|
|
|
- this.failedResponseHandler = createJsonErrorResponseHandler(errorStructure);
|
|
|
|
|
-
|
|
|
|
|
- this.supportsStructuredOutputs = config.supportsStructuredOutputs ?? false;
|
|
|
|
|
|
|
+ const errorStructure = config.errorStructure ?? defaultOpenAICompatibleErrorStructure
|
|
|
|
|
+ this.chunkSchema = createOpenAICompatibleChatChunkSchema(errorStructure.errorSchema)
|
|
|
|
|
+ this.failedResponseHandler = createJsonErrorResponseHandler(errorStructure)
|
|
|
|
|
+
|
|
|
|
|
+ this.supportsStructuredOutputs = config.supportsStructuredOutputs ?? false
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
get provider(): string {
|
|
get provider(): string {
|
|
|
- return this.config.provider;
|
|
|
|
|
|
|
+ return this.config.provider
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
private get providerOptionsName(): string {
|
|
private get providerOptionsName(): string {
|
|
|
- return this.config.provider.split('.')[0].trim();
|
|
|
|
|
|
|
+ return this.config.provider.split(".")[0].trim()
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
get supportedUrls() {
|
|
get supportedUrls() {
|
|
|
- return this.config.supportedUrls?.() ?? {};
|
|
|
|
|
|
|
+ return this.config.supportedUrls?.() ?? {}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
private async getArgs({
|
|
private async getArgs({
|
|
@@ -110,13 +98,13 @@ export class OpenAICompatibleChatLanguageModel implements LanguageModelV2 {
|
|
|
seed,
|
|
seed,
|
|
|
toolChoice,
|
|
toolChoice,
|
|
|
tools,
|
|
tools,
|
|
|
- }: Parameters<LanguageModelV2['doGenerate']>[0]) {
|
|
|
|
|
- const warnings: LanguageModelV2CallWarning[] = [];
|
|
|
|
|
|
|
+ }: Parameters<LanguageModelV2["doGenerate"]>[0]) {
|
|
|
|
|
+ const warnings: LanguageModelV2CallWarning[] = []
|
|
|
|
|
|
|
|
// Parse provider options
|
|
// Parse provider options
|
|
|
const compatibleOptions = Object.assign(
|
|
const compatibleOptions = Object.assign(
|
|
|
(await parseProviderOptions({
|
|
(await parseProviderOptions({
|
|
|
- provider: 'copilot',
|
|
|
|
|
|
|
+ provider: "copilot",
|
|
|
providerOptions,
|
|
providerOptions,
|
|
|
schema: openaiCompatibleProviderOptions,
|
|
schema: openaiCompatibleProviderOptions,
|
|
|
})) ?? {},
|
|
})) ?? {},
|
|
@@ -125,23 +113,18 @@ export class OpenAICompatibleChatLanguageModel implements LanguageModelV2 {
|
|
|
providerOptions,
|
|
providerOptions,
|
|
|
schema: openaiCompatibleProviderOptions,
|
|
schema: openaiCompatibleProviderOptions,
|
|
|
})) ?? {},
|
|
})) ?? {},
|
|
|
- );
|
|
|
|
|
|
|
+ )
|
|
|
|
|
|
|
|
if (topK != null) {
|
|
if (topK != null) {
|
|
|
- warnings.push({ type: 'unsupported-setting', setting: 'topK' });
|
|
|
|
|
|
|
+ warnings.push({ type: "unsupported-setting", setting: "topK" })
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- if (
|
|
|
|
|
- responseFormat?.type === 'json' &&
|
|
|
|
|
- responseFormat.schema != null &&
|
|
|
|
|
- !this.supportsStructuredOutputs
|
|
|
|
|
- ) {
|
|
|
|
|
|
|
+ if (responseFormat?.type === "json" && responseFormat.schema != null && !this.supportsStructuredOutputs) {
|
|
|
warnings.push({
|
|
warnings.push({
|
|
|
- type: 'unsupported-setting',
|
|
|
|
|
- setting: 'responseFormat',
|
|
|
|
|
- details:
|
|
|
|
|
- 'JSON response format schema is only supported with structuredOutputs',
|
|
|
|
|
- });
|
|
|
|
|
|
|
+ type: "unsupported-setting",
|
|
|
|
|
+ setting: "responseFormat",
|
|
|
|
|
+ details: "JSON response format schema is only supported with structuredOutputs",
|
|
|
|
|
+ })
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
const {
|
|
const {
|
|
@@ -151,7 +134,7 @@ export class OpenAICompatibleChatLanguageModel implements LanguageModelV2 {
|
|
|
} = prepareTools({
|
|
} = prepareTools({
|
|
|
tools,
|
|
tools,
|
|
|
toolChoice,
|
|
toolChoice,
|
|
|
- });
|
|
|
|
|
|
|
+ })
|
|
|
|
|
|
|
|
return {
|
|
return {
|
|
|
args: {
|
|
args: {
|
|
@@ -168,28 +151,24 @@ export class OpenAICompatibleChatLanguageModel implements LanguageModelV2 {
|
|
|
frequency_penalty: frequencyPenalty,
|
|
frequency_penalty: frequencyPenalty,
|
|
|
presence_penalty: presencePenalty,
|
|
presence_penalty: presencePenalty,
|
|
|
response_format:
|
|
response_format:
|
|
|
- responseFormat?.type === 'json'
|
|
|
|
|
- ? this.supportsStructuredOutputs === true &&
|
|
|
|
|
- responseFormat.schema != null
|
|
|
|
|
|
|
+ responseFormat?.type === "json"
|
|
|
|
|
+ ? this.supportsStructuredOutputs === true && responseFormat.schema != null
|
|
|
? {
|
|
? {
|
|
|
- type: 'json_schema',
|
|
|
|
|
|
|
+ type: "json_schema",
|
|
|
json_schema: {
|
|
json_schema: {
|
|
|
schema: responseFormat.schema,
|
|
schema: responseFormat.schema,
|
|
|
- name: responseFormat.name ?? 'response',
|
|
|
|
|
|
|
+ name: responseFormat.name ?? "response",
|
|
|
description: responseFormat.description,
|
|
description: responseFormat.description,
|
|
|
},
|
|
},
|
|
|
}
|
|
}
|
|
|
- : { type: 'json_object' }
|
|
|
|
|
|
|
+ : { type: "json_object" }
|
|
|
: undefined,
|
|
: undefined,
|
|
|
|
|
|
|
|
stop: stopSequences,
|
|
stop: stopSequences,
|
|
|
seed,
|
|
seed,
|
|
|
...Object.fromEntries(
|
|
...Object.fromEntries(
|
|
|
- Object.entries(
|
|
|
|
|
- providerOptions?.[this.providerOptionsName] ?? {},
|
|
|
|
|
- ).filter(
|
|
|
|
|
- ([key]) =>
|
|
|
|
|
- !Object.keys(openaiCompatibleProviderOptions.shape).includes(key),
|
|
|
|
|
|
|
+ Object.entries(providerOptions?.[this.providerOptionsName] ?? {}).filter(
|
|
|
|
|
+ ([key]) => !Object.keys(openaiCompatibleProviderOptions.shape).includes(key),
|
|
|
),
|
|
),
|
|
|
),
|
|
),
|
|
|
|
|
|
|
@@ -207,15 +186,15 @@ export class OpenAICompatibleChatLanguageModel implements LanguageModelV2 {
|
|
|
thinking_budget: compatibleOptions.thinking_budget,
|
|
thinking_budget: compatibleOptions.thinking_budget,
|
|
|
},
|
|
},
|
|
|
warnings: [...warnings, ...toolWarnings],
|
|
warnings: [...warnings, ...toolWarnings],
|
|
|
- };
|
|
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
async doGenerate(
|
|
async doGenerate(
|
|
|
- options: Parameters<LanguageModelV2['doGenerate']>[0],
|
|
|
|
|
- ): Promise<Awaited<ReturnType<LanguageModelV2['doGenerate']>>> {
|
|
|
|
|
- const { args, warnings } = await this.getArgs({ ...options });
|
|
|
|
|
|
|
+ options: Parameters<LanguageModelV2["doGenerate"]>[0],
|
|
|
|
|
+ ): Promise<Awaited<ReturnType<LanguageModelV2["doGenerate"]>>> {
|
|
|
|
|
+ const { args, warnings } = await this.getArgs({ ...options })
|
|
|
|
|
|
|
|
- const body = JSON.stringify(args);
|
|
|
|
|
|
|
+ const body = JSON.stringify(args)
|
|
|
|
|
|
|
|
const {
|
|
const {
|
|
|
responseHeaders,
|
|
responseHeaders,
|
|
@@ -223,50 +202,48 @@ export class OpenAICompatibleChatLanguageModel implements LanguageModelV2 {
|
|
|
rawValue: rawResponse,
|
|
rawValue: rawResponse,
|
|
|
} = await postJsonToApi({
|
|
} = await postJsonToApi({
|
|
|
url: this.config.url({
|
|
url: this.config.url({
|
|
|
- path: '/chat/completions',
|
|
|
|
|
|
|
+ path: "/chat/completions",
|
|
|
modelId: this.modelId,
|
|
modelId: this.modelId,
|
|
|
}),
|
|
}),
|
|
|
headers: combineHeaders(this.config.headers(), options.headers),
|
|
headers: combineHeaders(this.config.headers(), options.headers),
|
|
|
body: args,
|
|
body: args,
|
|
|
failedResponseHandler: this.failedResponseHandler,
|
|
failedResponseHandler: this.failedResponseHandler,
|
|
|
- successfulResponseHandler: createJsonResponseHandler(
|
|
|
|
|
- OpenAICompatibleChatResponseSchema,
|
|
|
|
|
- ),
|
|
|
|
|
|
|
+ successfulResponseHandler: createJsonResponseHandler(OpenAICompatibleChatResponseSchema),
|
|
|
abortSignal: options.abortSignal,
|
|
abortSignal: options.abortSignal,
|
|
|
fetch: this.config.fetch,
|
|
fetch: this.config.fetch,
|
|
|
- });
|
|
|
|
|
|
|
+ })
|
|
|
|
|
|
|
|
- const choice = responseBody.choices[0];
|
|
|
|
|
- const content: Array<LanguageModelV2Content> = [];
|
|
|
|
|
|
|
+ const choice = responseBody.choices[0]
|
|
|
|
|
+ const content: Array<LanguageModelV2Content> = []
|
|
|
|
|
|
|
|
// text content:
|
|
// text content:
|
|
|
- const text = choice.message.content;
|
|
|
|
|
|
|
+ const text = choice.message.content
|
|
|
if (text != null && text.length > 0) {
|
|
if (text != null && text.length > 0) {
|
|
|
- content.push({ type: 'text', text });
|
|
|
|
|
|
|
+ content.push({ type: "text", text })
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// reasoning content (Copilot uses reasoning_text):
|
|
// reasoning content (Copilot uses reasoning_text):
|
|
|
- const reasoning = choice.message.reasoning_text;
|
|
|
|
|
|
|
+ const reasoning = choice.message.reasoning_text
|
|
|
if (reasoning != null && reasoning.length > 0) {
|
|
if (reasoning != null && reasoning.length > 0) {
|
|
|
content.push({
|
|
content.push({
|
|
|
- type: 'reasoning',
|
|
|
|
|
|
|
+ type: "reasoning",
|
|
|
text: reasoning,
|
|
text: reasoning,
|
|
|
// Include reasoning_opaque for Copilot multi-turn reasoning
|
|
// Include reasoning_opaque for Copilot multi-turn reasoning
|
|
|
providerMetadata: choice.message.reasoning_opaque
|
|
providerMetadata: choice.message.reasoning_opaque
|
|
|
? { copilot: { reasoningOpaque: choice.message.reasoning_opaque } }
|
|
? { copilot: { reasoningOpaque: choice.message.reasoning_opaque } }
|
|
|
: undefined,
|
|
: undefined,
|
|
|
- });
|
|
|
|
|
|
|
+ })
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// tool calls:
|
|
// tool calls:
|
|
|
if (choice.message.tool_calls != null) {
|
|
if (choice.message.tool_calls != null) {
|
|
|
for (const toolCall of choice.message.tool_calls) {
|
|
for (const toolCall of choice.message.tool_calls) {
|
|
|
content.push({
|
|
content.push({
|
|
|
- type: 'tool-call',
|
|
|
|
|
|
|
+ type: "tool-call",
|
|
|
toolCallId: toolCall.id ?? generateId(),
|
|
toolCallId: toolCall.id ?? generateId(),
|
|
|
toolName: toolCall.function.name,
|
|
toolName: toolCall.function.name,
|
|
|
input: toolCall.function.arguments!,
|
|
input: toolCall.function.arguments!,
|
|
|
- });
|
|
|
|
|
|
|
+ })
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -276,16 +253,15 @@ export class OpenAICompatibleChatLanguageModel implements LanguageModelV2 {
|
|
|
...(await this.config.metadataExtractor?.extractMetadata?.({
|
|
...(await this.config.metadataExtractor?.extractMetadata?.({
|
|
|
parsedBody: rawResponse,
|
|
parsedBody: rawResponse,
|
|
|
})),
|
|
})),
|
|
|
- };
|
|
|
|
|
- const completionTokenDetails =
|
|
|
|
|
- responseBody.usage?.completion_tokens_details;
|
|
|
|
|
|
|
+ }
|
|
|
|
|
+ const completionTokenDetails = responseBody.usage?.completion_tokens_details
|
|
|
if (completionTokenDetails?.accepted_prediction_tokens != null) {
|
|
if (completionTokenDetails?.accepted_prediction_tokens != null) {
|
|
|
providerMetadata[this.providerOptionsName].acceptedPredictionTokens =
|
|
providerMetadata[this.providerOptionsName].acceptedPredictionTokens =
|
|
|
- completionTokenDetails?.accepted_prediction_tokens;
|
|
|
|
|
|
|
+ completionTokenDetails?.accepted_prediction_tokens
|
|
|
}
|
|
}
|
|
|
if (completionTokenDetails?.rejected_prediction_tokens != null) {
|
|
if (completionTokenDetails?.rejected_prediction_tokens != null) {
|
|
|
providerMetadata[this.providerOptionsName].rejectedPredictionTokens =
|
|
providerMetadata[this.providerOptionsName].rejectedPredictionTokens =
|
|
|
- completionTokenDetails?.rejected_prediction_tokens;
|
|
|
|
|
|
|
+ completionTokenDetails?.rejected_prediction_tokens
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
return {
|
|
return {
|
|
@@ -295,11 +271,8 @@ export class OpenAICompatibleChatLanguageModel implements LanguageModelV2 {
|
|
|
inputTokens: responseBody.usage?.prompt_tokens ?? undefined,
|
|
inputTokens: responseBody.usage?.prompt_tokens ?? undefined,
|
|
|
outputTokens: responseBody.usage?.completion_tokens ?? undefined,
|
|
outputTokens: responseBody.usage?.completion_tokens ?? undefined,
|
|
|
totalTokens: responseBody.usage?.total_tokens ?? undefined,
|
|
totalTokens: responseBody.usage?.total_tokens ?? undefined,
|
|
|
- reasoningTokens:
|
|
|
|
|
- responseBody.usage?.completion_tokens_details?.reasoning_tokens ??
|
|
|
|
|
- undefined,
|
|
|
|
|
- cachedInputTokens:
|
|
|
|
|
- responseBody.usage?.prompt_tokens_details?.cached_tokens ?? undefined,
|
|
|
|
|
|
|
+ reasoningTokens: responseBody.usage?.completion_tokens_details?.reasoning_tokens ?? undefined,
|
|
|
|
|
+ cachedInputTokens: responseBody.usage?.prompt_tokens_details?.cached_tokens ?? undefined,
|
|
|
},
|
|
},
|
|
|
providerMetadata,
|
|
providerMetadata,
|
|
|
request: { body },
|
|
request: { body },
|
|
@@ -309,65 +282,60 @@ export class OpenAICompatibleChatLanguageModel implements LanguageModelV2 {
|
|
|
body: rawResponse,
|
|
body: rawResponse,
|
|
|
},
|
|
},
|
|
|
warnings,
|
|
warnings,
|
|
|
- };
|
|
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
async doStream(
|
|
async doStream(
|
|
|
- options: Parameters<LanguageModelV2['doStream']>[0],
|
|
|
|
|
- ): Promise<Awaited<ReturnType<LanguageModelV2['doStream']>>> {
|
|
|
|
|
- const { args, warnings } = await this.getArgs({ ...options });
|
|
|
|
|
|
|
+ options: Parameters<LanguageModelV2["doStream"]>[0],
|
|
|
|
|
+ ): Promise<Awaited<ReturnType<LanguageModelV2["doStream"]>>> {
|
|
|
|
|
+ const { args, warnings } = await this.getArgs({ ...options })
|
|
|
|
|
|
|
|
const body = {
|
|
const body = {
|
|
|
...args,
|
|
...args,
|
|
|
stream: true,
|
|
stream: true,
|
|
|
|
|
|
|
|
// only include stream_options when in strict compatibility mode:
|
|
// only include stream_options when in strict compatibility mode:
|
|
|
- stream_options: this.config.includeUsage
|
|
|
|
|
- ? { include_usage: true }
|
|
|
|
|
- : undefined,
|
|
|
|
|
- };
|
|
|
|
|
|
|
+ stream_options: this.config.includeUsage ? { include_usage: true } : undefined,
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- const metadataExtractor =
|
|
|
|
|
- this.config.metadataExtractor?.createStreamExtractor();
|
|
|
|
|
|
|
+ const metadataExtractor = this.config.metadataExtractor?.createStreamExtractor()
|
|
|
|
|
|
|
|
const { responseHeaders, value: response } = await postJsonToApi({
|
|
const { responseHeaders, value: response } = await postJsonToApi({
|
|
|
url: this.config.url({
|
|
url: this.config.url({
|
|
|
- path: '/chat/completions',
|
|
|
|
|
|
|
+ path: "/chat/completions",
|
|
|
modelId: this.modelId,
|
|
modelId: this.modelId,
|
|
|
}),
|
|
}),
|
|
|
headers: combineHeaders(this.config.headers(), options.headers),
|
|
headers: combineHeaders(this.config.headers(), options.headers),
|
|
|
body,
|
|
body,
|
|
|
failedResponseHandler: this.failedResponseHandler,
|
|
failedResponseHandler: this.failedResponseHandler,
|
|
|
- successfulResponseHandler: createEventSourceResponseHandler(
|
|
|
|
|
- this.chunkSchema,
|
|
|
|
|
- ),
|
|
|
|
|
|
|
+ successfulResponseHandler: createEventSourceResponseHandler(this.chunkSchema),
|
|
|
abortSignal: options.abortSignal,
|
|
abortSignal: options.abortSignal,
|
|
|
fetch: this.config.fetch,
|
|
fetch: this.config.fetch,
|
|
|
- });
|
|
|
|
|
|
|
+ })
|
|
|
|
|
|
|
|
const toolCalls: Array<{
|
|
const toolCalls: Array<{
|
|
|
- id: string;
|
|
|
|
|
- type: 'function';
|
|
|
|
|
|
|
+ id: string
|
|
|
|
|
+ type: "function"
|
|
|
function: {
|
|
function: {
|
|
|
- name: string;
|
|
|
|
|
- arguments: string;
|
|
|
|
|
- };
|
|
|
|
|
- hasFinished: boolean;
|
|
|
|
|
- }> = [];
|
|
|
|
|
|
|
+ name: string
|
|
|
|
|
+ arguments: string
|
|
|
|
|
+ }
|
|
|
|
|
+ hasFinished: boolean
|
|
|
|
|
+ }> = []
|
|
|
|
|
|
|
|
- let finishReason: LanguageModelV2FinishReason = 'unknown';
|
|
|
|
|
|
|
+ let finishReason: LanguageModelV2FinishReason = "unknown"
|
|
|
const usage: {
|
|
const usage: {
|
|
|
- completionTokens: number | undefined;
|
|
|
|
|
|
|
+ completionTokens: number | undefined
|
|
|
completionTokensDetails: {
|
|
completionTokensDetails: {
|
|
|
- reasoningTokens: number | undefined;
|
|
|
|
|
- acceptedPredictionTokens: number | undefined;
|
|
|
|
|
- rejectedPredictionTokens: number | undefined;
|
|
|
|
|
- };
|
|
|
|
|
- promptTokens: number | undefined;
|
|
|
|
|
|
|
+ reasoningTokens: number | undefined
|
|
|
|
|
+ acceptedPredictionTokens: number | undefined
|
|
|
|
|
+ rejectedPredictionTokens: number | undefined
|
|
|
|
|
+ }
|
|
|
|
|
+ promptTokens: number | undefined
|
|
|
promptTokensDetails: {
|
|
promptTokensDetails: {
|
|
|
- cachedTokens: number | undefined;
|
|
|
|
|
- };
|
|
|
|
|
- totalTokens: number | undefined;
|
|
|
|
|
|
|
+ cachedTokens: number | undefined
|
|
|
|
|
+ }
|
|
|
|
|
+ totalTokens: number | undefined
|
|
|
} = {
|
|
} = {
|
|
|
completionTokens: undefined,
|
|
completionTokens: undefined,
|
|
|
completionTokensDetails: {
|
|
completionTokensDetails: {
|
|
@@ -380,54 +348,51 @@ export class OpenAICompatibleChatLanguageModel implements LanguageModelV2 {
|
|
|
cachedTokens: undefined,
|
|
cachedTokens: undefined,
|
|
|
},
|
|
},
|
|
|
totalTokens: undefined,
|
|
totalTokens: undefined,
|
|
|
- };
|
|
|
|
|
- let isFirstChunk = true;
|
|
|
|
|
- const providerOptionsName = this.providerOptionsName;
|
|
|
|
|
- let isActiveReasoning = false;
|
|
|
|
|
- let isActiveText = false;
|
|
|
|
|
- let reasoningOpaque: string | undefined;
|
|
|
|
|
|
|
+ }
|
|
|
|
|
+ let isFirstChunk = true
|
|
|
|
|
+ const providerOptionsName = this.providerOptionsName
|
|
|
|
|
+ let isActiveReasoning = false
|
|
|
|
|
+ let isActiveText = false
|
|
|
|
|
+ let reasoningOpaque: string | undefined
|
|
|
|
|
|
|
|
return {
|
|
return {
|
|
|
stream: response.pipeThrough(
|
|
stream: response.pipeThrough(
|
|
|
- new TransformStream<
|
|
|
|
|
- ParseResult<z.infer<typeof this.chunkSchema>>,
|
|
|
|
|
- LanguageModelV2StreamPart
|
|
|
|
|
- >({
|
|
|
|
|
|
|
+ new TransformStream<ParseResult<z.infer<typeof this.chunkSchema>>, LanguageModelV2StreamPart>({
|
|
|
start(controller) {
|
|
start(controller) {
|
|
|
- controller.enqueue({ type: 'stream-start', warnings });
|
|
|
|
|
|
|
+ controller.enqueue({ type: "stream-start", warnings })
|
|
|
},
|
|
},
|
|
|
|
|
|
|
|
// TODO we lost type safety on Chunk, most likely due to the error schema. MUST FIX
|
|
// TODO we lost type safety on Chunk, most likely due to the error schema. MUST FIX
|
|
|
transform(chunk, controller) {
|
|
transform(chunk, controller) {
|
|
|
// Emit raw chunk if requested (before anything else)
|
|
// Emit raw chunk if requested (before anything else)
|
|
|
if (options.includeRawChunks) {
|
|
if (options.includeRawChunks) {
|
|
|
- controller.enqueue({ type: 'raw', rawValue: chunk.rawValue });
|
|
|
|
|
|
|
+ controller.enqueue({ type: "raw", rawValue: chunk.rawValue })
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// handle failed chunk parsing / validation:
|
|
// handle failed chunk parsing / validation:
|
|
|
if (!chunk.success) {
|
|
if (!chunk.success) {
|
|
|
- finishReason = 'error';
|
|
|
|
|
- controller.enqueue({ type: 'error', error: chunk.error });
|
|
|
|
|
- return;
|
|
|
|
|
|
|
+ finishReason = "error"
|
|
|
|
|
+ controller.enqueue({ type: "error", error: chunk.error })
|
|
|
|
|
+ return
|
|
|
}
|
|
}
|
|
|
- const value = chunk.value;
|
|
|
|
|
|
|
+ const value = chunk.value
|
|
|
|
|
|
|
|
- metadataExtractor?.processChunk(chunk.rawValue);
|
|
|
|
|
|
|
+ metadataExtractor?.processChunk(chunk.rawValue)
|
|
|
|
|
|
|
|
// handle error chunks:
|
|
// handle error chunks:
|
|
|
- if ('error' in value) {
|
|
|
|
|
- finishReason = 'error';
|
|
|
|
|
- controller.enqueue({ type: 'error', error: value.error.message });
|
|
|
|
|
- return;
|
|
|
|
|
|
|
+ if ("error" in value) {
|
|
|
|
|
+ finishReason = "error"
|
|
|
|
|
+ controller.enqueue({ type: "error", error: value.error.message })
|
|
|
|
|
+ return
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
if (isFirstChunk) {
|
|
if (isFirstChunk) {
|
|
|
- isFirstChunk = false;
|
|
|
|
|
|
|
+ isFirstChunk = false
|
|
|
|
|
|
|
|
controller.enqueue({
|
|
controller.enqueue({
|
|
|
- type: 'response-metadata',
|
|
|
|
|
|
|
+ type: "response-metadata",
|
|
|
...getResponseMetadata(value),
|
|
...getResponseMetadata(value),
|
|
|
- });
|
|
|
|
|
|
|
+ })
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
if (value.usage != null) {
|
|
if (value.usage != null) {
|
|
@@ -437,46 +402,38 @@ export class OpenAICompatibleChatLanguageModel implements LanguageModelV2 {
|
|
|
total_tokens,
|
|
total_tokens,
|
|
|
prompt_tokens_details,
|
|
prompt_tokens_details,
|
|
|
completion_tokens_details,
|
|
completion_tokens_details,
|
|
|
- } = value.usage;
|
|
|
|
|
|
|
+ } = value.usage
|
|
|
|
|
|
|
|
- usage.promptTokens = prompt_tokens ?? undefined;
|
|
|
|
|
- usage.completionTokens = completion_tokens ?? undefined;
|
|
|
|
|
- usage.totalTokens = total_tokens ?? undefined;
|
|
|
|
|
|
|
+ usage.promptTokens = prompt_tokens ?? undefined
|
|
|
|
|
+ usage.completionTokens = completion_tokens ?? undefined
|
|
|
|
|
+ usage.totalTokens = total_tokens ?? undefined
|
|
|
if (completion_tokens_details?.reasoning_tokens != null) {
|
|
if (completion_tokens_details?.reasoning_tokens != null) {
|
|
|
- usage.completionTokensDetails.reasoningTokens =
|
|
|
|
|
- completion_tokens_details?.reasoning_tokens;
|
|
|
|
|
|
|
+ usage.completionTokensDetails.reasoningTokens = completion_tokens_details?.reasoning_tokens
|
|
|
}
|
|
}
|
|
|
- if (
|
|
|
|
|
- completion_tokens_details?.accepted_prediction_tokens != null
|
|
|
|
|
- ) {
|
|
|
|
|
|
|
+ if (completion_tokens_details?.accepted_prediction_tokens != null) {
|
|
|
usage.completionTokensDetails.acceptedPredictionTokens =
|
|
usage.completionTokensDetails.acceptedPredictionTokens =
|
|
|
- completion_tokens_details?.accepted_prediction_tokens;
|
|
|
|
|
|
|
+ completion_tokens_details?.accepted_prediction_tokens
|
|
|
}
|
|
}
|
|
|
- if (
|
|
|
|
|
- completion_tokens_details?.rejected_prediction_tokens != null
|
|
|
|
|
- ) {
|
|
|
|
|
|
|
+ if (completion_tokens_details?.rejected_prediction_tokens != null) {
|
|
|
usage.completionTokensDetails.rejectedPredictionTokens =
|
|
usage.completionTokensDetails.rejectedPredictionTokens =
|
|
|
- completion_tokens_details?.rejected_prediction_tokens;
|
|
|
|
|
|
|
+ completion_tokens_details?.rejected_prediction_tokens
|
|
|
}
|
|
}
|
|
|
if (prompt_tokens_details?.cached_tokens != null) {
|
|
if (prompt_tokens_details?.cached_tokens != null) {
|
|
|
- usage.promptTokensDetails.cachedTokens =
|
|
|
|
|
- prompt_tokens_details?.cached_tokens;
|
|
|
|
|
|
|
+ usage.promptTokensDetails.cachedTokens = prompt_tokens_details?.cached_tokens
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- const choice = value.choices[0];
|
|
|
|
|
|
|
+ const choice = value.choices[0]
|
|
|
|
|
|
|
|
if (choice?.finish_reason != null) {
|
|
if (choice?.finish_reason != null) {
|
|
|
- finishReason = mapOpenAICompatibleFinishReason(
|
|
|
|
|
- choice.finish_reason,
|
|
|
|
|
- );
|
|
|
|
|
|
|
+ finishReason = mapOpenAICompatibleFinishReason(choice.finish_reason)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
if (choice?.delta == null) {
|
|
if (choice?.delta == null) {
|
|
|
- return;
|
|
|
|
|
|
|
+ return
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- const delta = choice.delta;
|
|
|
|
|
|
|
+ const delta = choice.delta
|
|
|
|
|
|
|
|
// Capture reasoning_opaque for Copilot multi-turn reasoning
|
|
// Capture reasoning_opaque for Copilot multi-turn reasoning
|
|
|
if (delta.reasoning_opaque) {
|
|
if (delta.reasoning_opaque) {
|
|
@@ -484,28 +441,28 @@ export class OpenAICompatibleChatLanguageModel implements LanguageModelV2 {
|
|
|
throw new InvalidResponseDataError({
|
|
throw new InvalidResponseDataError({
|
|
|
data: delta,
|
|
data: delta,
|
|
|
message:
|
|
message:
|
|
|
- 'Multiple reasoning_opaque values received in a single response. Only one thinking part per response is supported.',
|
|
|
|
|
- });
|
|
|
|
|
|
|
+ "Multiple reasoning_opaque values received in a single response. Only one thinking part per response is supported.",
|
|
|
|
|
+ })
|
|
|
}
|
|
}
|
|
|
- reasoningOpaque = delta.reasoning_opaque;
|
|
|
|
|
|
|
+ reasoningOpaque = delta.reasoning_opaque
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// enqueue reasoning before text deltas (Copilot uses reasoning_text):
|
|
// enqueue reasoning before text deltas (Copilot uses reasoning_text):
|
|
|
- const reasoningContent = delta.reasoning_text;
|
|
|
|
|
|
|
+ const reasoningContent = delta.reasoning_text
|
|
|
if (reasoningContent) {
|
|
if (reasoningContent) {
|
|
|
if (!isActiveReasoning) {
|
|
if (!isActiveReasoning) {
|
|
|
controller.enqueue({
|
|
controller.enqueue({
|
|
|
- type: 'reasoning-start',
|
|
|
|
|
- id: 'reasoning-0',
|
|
|
|
|
- });
|
|
|
|
|
- isActiveReasoning = true;
|
|
|
|
|
|
|
+ type: "reasoning-start",
|
|
|
|
|
+ id: "reasoning-0",
|
|
|
|
|
+ })
|
|
|
|
|
+ isActiveReasoning = true
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
controller.enqueue({
|
|
controller.enqueue({
|
|
|
- type: 'reasoning-delta',
|
|
|
|
|
- id: 'reasoning-0',
|
|
|
|
|
|
|
+ type: "reasoning-delta",
|
|
|
|
|
+ id: "reasoning-0",
|
|
|
delta: reasoningContent,
|
|
delta: reasoningContent,
|
|
|
- });
|
|
|
|
|
|
|
+ })
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
if (delta.content) {
|
|
if (delta.content) {
|
|
@@ -513,25 +470,23 @@ export class OpenAICompatibleChatLanguageModel implements LanguageModelV2 {
|
|
|
// This handles the case where reasoning_opaque and content come in the same chunk
|
|
// This handles the case where reasoning_opaque and content come in the same chunk
|
|
|
if (isActiveReasoning && !isActiveText) {
|
|
if (isActiveReasoning && !isActiveText) {
|
|
|
controller.enqueue({
|
|
controller.enqueue({
|
|
|
- type: 'reasoning-end',
|
|
|
|
|
- id: 'reasoning-0',
|
|
|
|
|
- providerMetadata: reasoningOpaque
|
|
|
|
|
- ? { copilot: { reasoningOpaque } }
|
|
|
|
|
- : undefined,
|
|
|
|
|
- });
|
|
|
|
|
- isActiveReasoning = false;
|
|
|
|
|
|
|
+ type: "reasoning-end",
|
|
|
|
|
+ id: "reasoning-0",
|
|
|
|
|
+ providerMetadata: reasoningOpaque ? { copilot: { reasoningOpaque } } : undefined,
|
|
|
|
|
+ })
|
|
|
|
|
+ isActiveReasoning = false
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
if (!isActiveText) {
|
|
if (!isActiveText) {
|
|
|
- controller.enqueue({ type: 'text-start', id: 'txt-0' });
|
|
|
|
|
- isActiveText = true;
|
|
|
|
|
|
|
+ controller.enqueue({ type: "text-start", id: "txt-0" })
|
|
|
|
|
+ isActiveText = true
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
controller.enqueue({
|
|
controller.enqueue({
|
|
|
- type: 'text-delta',
|
|
|
|
|
- id: 'txt-0',
|
|
|
|
|
|
|
+ type: "text-delta",
|
|
|
|
|
+ id: "txt-0",
|
|
|
delta: delta.content,
|
|
delta: delta.content,
|
|
|
- });
|
|
|
|
|
|
|
+ })
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
if (delta.tool_calls != null) {
|
|
if (delta.tool_calls != null) {
|
|
@@ -539,102 +494,96 @@ export class OpenAICompatibleChatLanguageModel implements LanguageModelV2 {
|
|
|
// This handles the case where reasoning goes directly to tool calls with no content
|
|
// This handles the case where reasoning goes directly to tool calls with no content
|
|
|
if (isActiveReasoning) {
|
|
if (isActiveReasoning) {
|
|
|
controller.enqueue({
|
|
controller.enqueue({
|
|
|
- type: 'reasoning-end',
|
|
|
|
|
- id: 'reasoning-0',
|
|
|
|
|
- providerMetadata: reasoningOpaque
|
|
|
|
|
- ? { copilot: { reasoningOpaque } }
|
|
|
|
|
- : undefined,
|
|
|
|
|
- });
|
|
|
|
|
- isActiveReasoning = false;
|
|
|
|
|
|
|
+ type: "reasoning-end",
|
|
|
|
|
+ id: "reasoning-0",
|
|
|
|
|
+ providerMetadata: reasoningOpaque ? { copilot: { reasoningOpaque } } : undefined,
|
|
|
|
|
+ })
|
|
|
|
|
+ isActiveReasoning = false
|
|
|
}
|
|
}
|
|
|
for (const toolCallDelta of delta.tool_calls) {
|
|
for (const toolCallDelta of delta.tool_calls) {
|
|
|
- const index = toolCallDelta.index;
|
|
|
|
|
|
|
+ const index = toolCallDelta.index
|
|
|
|
|
|
|
|
if (toolCalls[index] == null) {
|
|
if (toolCalls[index] == null) {
|
|
|
if (toolCallDelta.id == null) {
|
|
if (toolCallDelta.id == null) {
|
|
|
throw new InvalidResponseDataError({
|
|
throw new InvalidResponseDataError({
|
|
|
data: toolCallDelta,
|
|
data: toolCallDelta,
|
|
|
message: `Expected 'id' to be a string.`,
|
|
message: `Expected 'id' to be a string.`,
|
|
|
- });
|
|
|
|
|
|
|
+ })
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
if (toolCallDelta.function?.name == null) {
|
|
if (toolCallDelta.function?.name == null) {
|
|
|
throw new InvalidResponseDataError({
|
|
throw new InvalidResponseDataError({
|
|
|
data: toolCallDelta,
|
|
data: toolCallDelta,
|
|
|
message: `Expected 'function.name' to be a string.`,
|
|
message: `Expected 'function.name' to be a string.`,
|
|
|
- });
|
|
|
|
|
|
|
+ })
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
controller.enqueue({
|
|
controller.enqueue({
|
|
|
- type: 'tool-input-start',
|
|
|
|
|
|
|
+ type: "tool-input-start",
|
|
|
id: toolCallDelta.id,
|
|
id: toolCallDelta.id,
|
|
|
toolName: toolCallDelta.function.name,
|
|
toolName: toolCallDelta.function.name,
|
|
|
- });
|
|
|
|
|
|
|
+ })
|
|
|
|
|
|
|
|
toolCalls[index] = {
|
|
toolCalls[index] = {
|
|
|
id: toolCallDelta.id,
|
|
id: toolCallDelta.id,
|
|
|
- type: 'function',
|
|
|
|
|
|
|
+ type: "function",
|
|
|
function: {
|
|
function: {
|
|
|
name: toolCallDelta.function.name,
|
|
name: toolCallDelta.function.name,
|
|
|
- arguments: toolCallDelta.function.arguments ?? '',
|
|
|
|
|
|
|
+ arguments: toolCallDelta.function.arguments ?? "",
|
|
|
},
|
|
},
|
|
|
hasFinished: false,
|
|
hasFinished: false,
|
|
|
- };
|
|
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- const toolCall = toolCalls[index];
|
|
|
|
|
|
|
+ const toolCall = toolCalls[index]
|
|
|
|
|
|
|
|
- if (
|
|
|
|
|
- toolCall.function?.name != null &&
|
|
|
|
|
- toolCall.function?.arguments != null
|
|
|
|
|
- ) {
|
|
|
|
|
|
|
+ if (toolCall.function?.name != null && toolCall.function?.arguments != null) {
|
|
|
// send delta if the argument text has already started:
|
|
// send delta if the argument text has already started:
|
|
|
if (toolCall.function.arguments.length > 0) {
|
|
if (toolCall.function.arguments.length > 0) {
|
|
|
controller.enqueue({
|
|
controller.enqueue({
|
|
|
- type: 'tool-input-delta',
|
|
|
|
|
|
|
+ type: "tool-input-delta",
|
|
|
id: toolCall.id,
|
|
id: toolCall.id,
|
|
|
delta: toolCall.function.arguments,
|
|
delta: toolCall.function.arguments,
|
|
|
- });
|
|
|
|
|
|
|
+ })
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// check if tool call is complete
|
|
// check if tool call is complete
|
|
|
// (some providers send the full tool call in one chunk):
|
|
// (some providers send the full tool call in one chunk):
|
|
|
if (isParsableJson(toolCall.function.arguments)) {
|
|
if (isParsableJson(toolCall.function.arguments)) {
|
|
|
controller.enqueue({
|
|
controller.enqueue({
|
|
|
- type: 'tool-input-end',
|
|
|
|
|
|
|
+ type: "tool-input-end",
|
|
|
id: toolCall.id,
|
|
id: toolCall.id,
|
|
|
- });
|
|
|
|
|
|
|
+ })
|
|
|
|
|
|
|
|
controller.enqueue({
|
|
controller.enqueue({
|
|
|
- type: 'tool-call',
|
|
|
|
|
|
|
+ type: "tool-call",
|
|
|
toolCallId: toolCall.id ?? generateId(),
|
|
toolCallId: toolCall.id ?? generateId(),
|
|
|
toolName: toolCall.function.name,
|
|
toolName: toolCall.function.name,
|
|
|
input: toolCall.function.arguments,
|
|
input: toolCall.function.arguments,
|
|
|
- });
|
|
|
|
|
- toolCall.hasFinished = true;
|
|
|
|
|
|
|
+ })
|
|
|
|
|
+ toolCall.hasFinished = true
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- continue;
|
|
|
|
|
|
|
+ continue
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// existing tool call, merge if not finished
|
|
// existing tool call, merge if not finished
|
|
|
- const toolCall = toolCalls[index];
|
|
|
|
|
|
|
+ const toolCall = toolCalls[index]
|
|
|
|
|
|
|
|
if (toolCall.hasFinished) {
|
|
if (toolCall.hasFinished) {
|
|
|
- continue;
|
|
|
|
|
|
|
+ continue
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
if (toolCallDelta.function?.arguments != null) {
|
|
if (toolCallDelta.function?.arguments != null) {
|
|
|
- toolCall.function!.arguments +=
|
|
|
|
|
- toolCallDelta.function?.arguments ?? '';
|
|
|
|
|
|
|
+ toolCall.function!.arguments += toolCallDelta.function?.arguments ?? ""
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// send delta
|
|
// send delta
|
|
|
controller.enqueue({
|
|
controller.enqueue({
|
|
|
- type: 'tool-input-delta',
|
|
|
|
|
|
|
+ type: "tool-input-delta",
|
|
|
id: toolCall.id,
|
|
id: toolCall.id,
|
|
|
- delta: toolCallDelta.function.arguments ?? '',
|
|
|
|
|
- });
|
|
|
|
|
|
|
+ delta: toolCallDelta.function.arguments ?? "",
|
|
|
|
|
+ })
|
|
|
|
|
|
|
|
// check if tool call is complete
|
|
// check if tool call is complete
|
|
|
if (
|
|
if (
|
|
@@ -643,17 +592,17 @@ export class OpenAICompatibleChatLanguageModel implements LanguageModelV2 {
|
|
|
isParsableJson(toolCall.function.arguments)
|
|
isParsableJson(toolCall.function.arguments)
|
|
|
) {
|
|
) {
|
|
|
controller.enqueue({
|
|
controller.enqueue({
|
|
|
- type: 'tool-input-end',
|
|
|
|
|
|
|
+ type: "tool-input-end",
|
|
|
id: toolCall.id,
|
|
id: toolCall.id,
|
|
|
- });
|
|
|
|
|
|
|
+ })
|
|
|
|
|
|
|
|
controller.enqueue({
|
|
controller.enqueue({
|
|
|
- type: 'tool-call',
|
|
|
|
|
|
|
+ type: "tool-call",
|
|
|
toolCallId: toolCall.id ?? generateId(),
|
|
toolCallId: toolCall.id ?? generateId(),
|
|
|
toolName: toolCall.function.name,
|
|
toolName: toolCall.function.name,
|
|
|
input: toolCall.function.arguments,
|
|
input: toolCall.function.arguments,
|
|
|
- });
|
|
|
|
|
- toolCall.hasFinished = true;
|
|
|
|
|
|
|
+ })
|
|
|
|
|
+ toolCall.hasFinished = true
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
@@ -662,77 +611,65 @@ export class OpenAICompatibleChatLanguageModel implements LanguageModelV2 {
|
|
|
flush(controller) {
|
|
flush(controller) {
|
|
|
if (isActiveReasoning) {
|
|
if (isActiveReasoning) {
|
|
|
controller.enqueue({
|
|
controller.enqueue({
|
|
|
- type: 'reasoning-end',
|
|
|
|
|
- id: 'reasoning-0',
|
|
|
|
|
|
|
+ type: "reasoning-end",
|
|
|
|
|
+ id: "reasoning-0",
|
|
|
// Include reasoning_opaque for Copilot multi-turn reasoning
|
|
// Include reasoning_opaque for Copilot multi-turn reasoning
|
|
|
- providerMetadata: reasoningOpaque
|
|
|
|
|
- ? { copilot: { reasoningOpaque } }
|
|
|
|
|
- : undefined,
|
|
|
|
|
- });
|
|
|
|
|
|
|
+ providerMetadata: reasoningOpaque ? { copilot: { reasoningOpaque } } : undefined,
|
|
|
|
|
+ })
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
if (isActiveText) {
|
|
if (isActiveText) {
|
|
|
- controller.enqueue({ type: 'text-end', id: 'txt-0' });
|
|
|
|
|
|
|
+ controller.enqueue({ type: "text-end", id: "txt-0" })
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// go through all tool calls and send the ones that are not finished
|
|
// go through all tool calls and send the ones that are not finished
|
|
|
- for (const toolCall of toolCalls.filter(
|
|
|
|
|
- toolCall => !toolCall.hasFinished,
|
|
|
|
|
- )) {
|
|
|
|
|
|
|
+ for (const toolCall of toolCalls.filter((toolCall) => !toolCall.hasFinished)) {
|
|
|
controller.enqueue({
|
|
controller.enqueue({
|
|
|
- type: 'tool-input-end',
|
|
|
|
|
|
|
+ type: "tool-input-end",
|
|
|
id: toolCall.id,
|
|
id: toolCall.id,
|
|
|
- });
|
|
|
|
|
|
|
+ })
|
|
|
|
|
|
|
|
controller.enqueue({
|
|
controller.enqueue({
|
|
|
- type: 'tool-call',
|
|
|
|
|
|
|
+ type: "tool-call",
|
|
|
toolCallId: toolCall.id ?? generateId(),
|
|
toolCallId: toolCall.id ?? generateId(),
|
|
|
toolName: toolCall.function.name,
|
|
toolName: toolCall.function.name,
|
|
|
input: toolCall.function.arguments,
|
|
input: toolCall.function.arguments,
|
|
|
- });
|
|
|
|
|
|
|
+ })
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
const providerMetadata: SharedV2ProviderMetadata = {
|
|
const providerMetadata: SharedV2ProviderMetadata = {
|
|
|
[providerOptionsName]: {},
|
|
[providerOptionsName]: {},
|
|
|
// Include reasoning_opaque for Copilot multi-turn reasoning
|
|
// Include reasoning_opaque for Copilot multi-turn reasoning
|
|
|
- ...(reasoningOpaque
|
|
|
|
|
- ? { copilot: { reasoningOpaque } }
|
|
|
|
|
- : {}),
|
|
|
|
|
|
|
+ ...(reasoningOpaque ? { copilot: { reasoningOpaque } } : {}),
|
|
|
...metadataExtractor?.buildMetadata(),
|
|
...metadataExtractor?.buildMetadata(),
|
|
|
- };
|
|
|
|
|
- if (
|
|
|
|
|
- usage.completionTokensDetails.acceptedPredictionTokens != null
|
|
|
|
|
- ) {
|
|
|
|
|
|
|
+ }
|
|
|
|
|
+ if (usage.completionTokensDetails.acceptedPredictionTokens != null) {
|
|
|
providerMetadata[providerOptionsName].acceptedPredictionTokens =
|
|
providerMetadata[providerOptionsName].acceptedPredictionTokens =
|
|
|
- usage.completionTokensDetails.acceptedPredictionTokens;
|
|
|
|
|
|
|
+ usage.completionTokensDetails.acceptedPredictionTokens
|
|
|
}
|
|
}
|
|
|
- if (
|
|
|
|
|
- usage.completionTokensDetails.rejectedPredictionTokens != null
|
|
|
|
|
- ) {
|
|
|
|
|
|
|
+ if (usage.completionTokensDetails.rejectedPredictionTokens != null) {
|
|
|
providerMetadata[providerOptionsName].rejectedPredictionTokens =
|
|
providerMetadata[providerOptionsName].rejectedPredictionTokens =
|
|
|
- usage.completionTokensDetails.rejectedPredictionTokens;
|
|
|
|
|
|
|
+ usage.completionTokensDetails.rejectedPredictionTokens
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
controller.enqueue({
|
|
controller.enqueue({
|
|
|
- type: 'finish',
|
|
|
|
|
|
|
+ type: "finish",
|
|
|
finishReason,
|
|
finishReason,
|
|
|
usage: {
|
|
usage: {
|
|
|
inputTokens: usage.promptTokens ?? undefined,
|
|
inputTokens: usage.promptTokens ?? undefined,
|
|
|
outputTokens: usage.completionTokens ?? undefined,
|
|
outputTokens: usage.completionTokens ?? undefined,
|
|
|
totalTokens: usage.totalTokens ?? undefined,
|
|
totalTokens: usage.totalTokens ?? undefined,
|
|
|
- reasoningTokens:
|
|
|
|
|
- usage.completionTokensDetails.reasoningTokens ?? undefined,
|
|
|
|
|
- cachedInputTokens:
|
|
|
|
|
- usage.promptTokensDetails.cachedTokens ?? undefined,
|
|
|
|
|
|
|
+ reasoningTokens: usage.completionTokensDetails.reasoningTokens ?? undefined,
|
|
|
|
|
+ cachedInputTokens: usage.promptTokensDetails.cachedTokens ?? undefined,
|
|
|
},
|
|
},
|
|
|
providerMetadata,
|
|
providerMetadata,
|
|
|
- });
|
|
|
|
|
|
|
+ })
|
|
|
},
|
|
},
|
|
|
}),
|
|
}),
|
|
|
),
|
|
),
|
|
|
request: { body },
|
|
request: { body },
|
|
|
response: { headers: responseHeaders },
|
|
response: { headers: responseHeaders },
|
|
|
- };
|
|
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -754,7 +691,7 @@ const openaiCompatibleTokenUsageSchema = z
|
|
|
})
|
|
})
|
|
|
.nullish(),
|
|
.nullish(),
|
|
|
})
|
|
})
|
|
|
- .nullish();
|
|
|
|
|
|
|
+ .nullish()
|
|
|
|
|
|
|
|
// limited version of the schema, focussed on what is needed for the implementation
|
|
// limited version of the schema, focussed on what is needed for the implementation
|
|
|
// this approach limits breakages when the API changes and increases efficiency
|
|
// this approach limits breakages when the API changes and increases efficiency
|
|
@@ -765,7 +702,7 @@ const OpenAICompatibleChatResponseSchema = z.object({
|
|
|
choices: z.array(
|
|
choices: z.array(
|
|
|
z.object({
|
|
z.object({
|
|
|
message: z.object({
|
|
message: z.object({
|
|
|
- role: z.literal('assistant').nullish(),
|
|
|
|
|
|
|
+ role: z.literal("assistant").nullish(),
|
|
|
content: z.string().nullish(),
|
|
content: z.string().nullish(),
|
|
|
// Copilot-specific reasoning fields
|
|
// Copilot-specific reasoning fields
|
|
|
reasoning_text: z.string().nullish(),
|
|
reasoning_text: z.string().nullish(),
|
|
@@ -786,15 +723,11 @@ const OpenAICompatibleChatResponseSchema = z.object({
|
|
|
}),
|
|
}),
|
|
|
),
|
|
),
|
|
|
usage: openaiCompatibleTokenUsageSchema,
|
|
usage: openaiCompatibleTokenUsageSchema,
|
|
|
-});
|
|
|
|
|
|
|
+})
|
|
|
|
|
|
|
|
// limited version of the schema, focussed on what is needed for the implementation
|
|
// limited version of the schema, focussed on what is needed for the implementation
|
|
|
// this approach limits breakages when the API changes and increases efficiency
|
|
// this approach limits breakages when the API changes and increases efficiency
|
|
|
-const createOpenAICompatibleChatChunkSchema = <
|
|
|
|
|
- ERROR_SCHEMA extends z.core.$ZodType,
|
|
|
|
|
->(
|
|
|
|
|
- errorSchema: ERROR_SCHEMA,
|
|
|
|
|
-) =>
|
|
|
|
|
|
|
+const createOpenAICompatibleChatChunkSchema = <ERROR_SCHEMA extends z.core.$ZodType>(errorSchema: ERROR_SCHEMA) =>
|
|
|
z.union([
|
|
z.union([
|
|
|
z.object({
|
|
z.object({
|
|
|
id: z.string().nullish(),
|
|
id: z.string().nullish(),
|
|
@@ -804,7 +737,7 @@ const createOpenAICompatibleChatChunkSchema = <
|
|
|
z.object({
|
|
z.object({
|
|
|
delta: z
|
|
delta: z
|
|
|
.object({
|
|
.object({
|
|
|
- role: z.enum(['assistant']).nullish(),
|
|
|
|
|
|
|
+ role: z.enum(["assistant"]).nullish(),
|
|
|
content: z.string().nullish(),
|
|
content: z.string().nullish(),
|
|
|
// Copilot-specific reasoning fields
|
|
// Copilot-specific reasoning fields
|
|
|
reasoning_text: z.string().nullish(),
|
|
reasoning_text: z.string().nullish(),
|
|
@@ -829,4 +762,4 @@ const createOpenAICompatibleChatChunkSchema = <
|
|
|
usage: openaiCompatibleTokenUsageSchema,
|
|
usage: openaiCompatibleTokenUsageSchema,
|
|
|
}),
|
|
}),
|
|
|
errorSchema,
|
|
errorSchema,
|
|
|
- ]);
|
|
|
|
|
|
|
+ ])
|