opencodekit 0.21.3 → 0.21.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/dist/index.js +1 -1
  2. package/dist/template/.opencode/.template-manifest.json +8 -8
  3. package/dist/template/.opencode/.version +1 -1
  4. package/dist/template/.opencode/AGENTS.md +55 -36
  5. package/dist/template/.opencode/agent/build.md +13 -3
  6. package/dist/template/.opencode/agent/explore.md +14 -0
  7. package/dist/template/.opencode/agent/general.md +13 -2
  8. package/dist/template/.opencode/agent/painter.md +9 -0
  9. package/dist/template/.opencode/agent/plan.md +26 -4
  10. package/dist/template/.opencode/agent/review.md +10 -0
  11. package/dist/template/.opencode/agent/scout.md +16 -1
  12. package/dist/template/.opencode/agent/vision.md +23 -0
  13. package/dist/template/.opencode/command/design.md +27 -8
  14. package/dist/template/.opencode/command/plan.md +22 -0
  15. package/dist/template/.opencode/command/ship.md +31 -5
  16. package/dist/template/.opencode/command/status.md +14 -5
  17. package/dist/template/.opencode/command/ui-review.md +38 -18
  18. package/dist/template/.opencode/command/ui-slop-check.md +30 -7
  19. package/dist/template/.opencode/command/verify.md +3 -0
  20. package/dist/template/.opencode/memory.db +0 -0
  21. package/dist/template/.opencode/memory.db-shm +0 -0
  22. package/dist/template/.opencode/memory.db-wal +0 -0
  23. package/dist/template/.opencode/opencode.json +164 -329
  24. package/dist/template/.opencode/plugin/sdk/copilot/chat/convert-to-openai-compatible-chat-messages.ts +162 -168
  25. package/dist/template/.opencode/plugin/sdk/copilot/chat/map-openai-compatible-finish-reason.ts +16 -16
  26. package/dist/template/.opencode/plugin/sdk/copilot/chat/openai-compatible-chat-language-model.ts +807 -805
  27. package/dist/template/.opencode/plugin/sdk/copilot/chat/openai-compatible-prepare-tools.ts +77 -77
  28. package/dist/template/.opencode/plugin/sdk/copilot/copilot-provider.ts +75 -80
  29. package/dist/template/.opencode/skill/playwright/SKILL.md +51 -2
  30. package/dist/template/.opencode/skill/portless/SKILL.md +109 -0
  31. package/dist/template/.opencode/skill/terse-output-mode/SKILL.md +95 -0
  32. package/dist/template/.opencode/skill/think-in-code/SKILL.md +136 -0
  33. package/dist/template/.opencode/skill/ux-quality-gates/SKILL.md +137 -0
  34. package/package.json +1 -1
@@ -1,833 +1,835 @@
1
1
  import {
2
- type APICallError,
3
- InvalidResponseDataError,
4
- type LanguageModelV2,
5
- type LanguageModelV2CallWarning,
6
- type LanguageModelV2Content,
7
- type LanguageModelV2FinishReason,
8
- type LanguageModelV2StreamPart,
9
- type SharedV2ProviderMetadata,
2
+ APICallError,
3
+ InvalidResponseDataError,
4
+ type LanguageModelV3,
5
+ type LanguageModelV3CallOptions,
6
+ type LanguageModelV3Content,
7
+ type LanguageModelV3StreamPart,
8
+ type SharedV3ProviderMetadata,
9
+ type SharedV3Warning,
10
10
  } from "@ai-sdk/provider";
11
11
  import {
12
- combineHeaders,
13
- createEventSourceResponseHandler,
14
- createJsonErrorResponseHandler,
15
- createJsonResponseHandler,
16
- type FetchFunction,
17
- generateId,
18
- isParsableJson,
19
- type ParseResult,
20
- parseProviderOptions,
21
- postJsonToApi,
22
- type ResponseHandler,
12
+ combineHeaders,
13
+ createEventSourceResponseHandler,
14
+ createJsonErrorResponseHandler,
15
+ createJsonResponseHandler,
16
+ type FetchFunction,
17
+ generateId,
18
+ isParsableJson,
19
+ parseProviderOptions,
20
+ type ParseResult,
21
+ postJsonToApi,
22
+ type ResponseHandler,
23
23
  } from "@ai-sdk/provider-utils";
24
24
  import { z } from "zod/v4";
25
- import {
26
- defaultOpenAICompatibleErrorStructure,
27
- type ProviderErrorStructure,
28
- } from "../openai-compatible-error.js";
29
25
  import { convertToOpenAICompatibleChatMessages } from "./convert-to-openai-compatible-chat-messages.js";
30
26
  import { getResponseMetadata } from "./get-response-metadata.js";
31
27
  import { mapOpenAICompatibleFinishReason } from "./map-openai-compatible-finish-reason.js";
32
28
  import {
33
- type OpenAICompatibleChatModelId,
34
- openaiCompatibleProviderOptions,
29
+ type OpenAICompatibleChatModelId,
30
+ openaiCompatibleProviderOptions,
35
31
  } from "./openai-compatible-chat-options.js";
32
+ import {
33
+ defaultOpenAICompatibleErrorStructure,
34
+ type ProviderErrorStructure,
35
+ } from "../openai-compatible-error.js";
36
36
  import type { MetadataExtractor } from "./openai-compatible-metadata-extractor.js";
37
37
  import { prepareTools } from "./openai-compatible-prepare-tools.js";
38
38
 
39
39
  export type OpenAICompatibleChatConfig = {
40
- provider: string;
41
- headers: () => Record<string, string | undefined>;
42
- url: (options: { modelId: string; path: string }) => string;
43
- fetch?: FetchFunction;
44
- includeUsage?: boolean;
45
- errorStructure?: ProviderErrorStructure<any>;
46
- metadataExtractor?: MetadataExtractor;
47
-
48
- /**
49
- * Whether the model supports structured outputs.
50
- */
51
- supportsStructuredOutputs?: boolean;
52
-
53
- /**
54
- * The supported URLs for the model.
55
- */
56
- supportedUrls?: () => LanguageModelV2["supportedUrls"];
40
+ provider: string;
41
+ headers: () => Record<string, string | undefined>;
42
+ url: (options: { modelId: string; path: string }) => string;
43
+ fetch?: FetchFunction;
44
+ includeUsage?: boolean;
45
+ errorStructure?: ProviderErrorStructure<any>;
46
+ metadataExtractor?: MetadataExtractor;
47
+
48
+ /**
49
+ * Whether the model supports structured outputs.
50
+ */
51
+ supportsStructuredOutputs?: boolean;
52
+
53
+ /**
54
+ * The supported URLs for the model.
55
+ */
56
+ supportedUrls?: () => LanguageModelV3["supportedUrls"];
57
57
  };
58
58
 
59
- export class OpenAICompatibleChatLanguageModel implements LanguageModelV2 {
60
- readonly specificationVersion = "v2";
61
-
62
- readonly supportsStructuredOutputs: boolean;
63
-
64
- readonly modelId: OpenAICompatibleChatModelId;
65
- private readonly config: OpenAICompatibleChatConfig;
66
- private readonly failedResponseHandler: ResponseHandler<APICallError>;
67
- private readonly chunkSchema; // type inferred via constructor
68
-
69
- constructor(
70
- modelId: OpenAICompatibleChatModelId,
71
- config: OpenAICompatibleChatConfig,
72
- ) {
73
- this.modelId = modelId;
74
- this.config = config;
75
-
76
- // initialize error handling:
77
- const errorStructure =
78
- config.errorStructure ?? defaultOpenAICompatibleErrorStructure;
79
- this.chunkSchema = createOpenAICompatibleChatChunkSchema(
80
- errorStructure.errorSchema,
81
- );
82
- this.failedResponseHandler = createJsonErrorResponseHandler(errorStructure);
83
-
84
- this.supportsStructuredOutputs = config.supportsStructuredOutputs ?? false;
85
- }
86
-
87
- get provider(): string {
88
- return this.config.provider;
89
- }
90
-
91
- private get providerOptionsName(): string {
92
- return this.config.provider.split(".")[0].trim();
93
- }
94
-
95
- get supportedUrls() {
96
- return this.config.supportedUrls?.() ?? {};
97
- }
98
-
99
- private async getArgs({
100
- prompt,
101
- maxOutputTokens,
102
- temperature,
103
- topP,
104
- topK,
105
- frequencyPenalty,
106
- presencePenalty,
107
- providerOptions,
108
- stopSequences,
109
- responseFormat,
110
- seed,
111
- toolChoice,
112
- tools,
113
- }: Parameters<LanguageModelV2["doGenerate"]>[0]) {
114
- const warnings: LanguageModelV2CallWarning[] = [];
115
-
116
- // Parse provider options
117
- const compatibleOptions = Object.assign(
118
- (await parseProviderOptions({
119
- provider: "copilot",
120
- providerOptions,
121
- schema: openaiCompatibleProviderOptions,
122
- })) ?? {},
123
- (await parseProviderOptions({
124
- provider: this.providerOptionsName,
125
- providerOptions,
126
- schema: openaiCompatibleProviderOptions,
127
- })) ?? {},
128
- );
129
-
130
- if (topK != null) {
131
- warnings.push({ type: "unsupported-setting", setting: "topK" });
132
- }
133
-
134
- if (
135
- responseFormat?.type === "json" &&
136
- responseFormat.schema != null &&
137
- !this.supportsStructuredOutputs
138
- ) {
139
- warnings.push({
140
- type: "unsupported-setting",
141
- setting: "responseFormat",
142
- details:
143
- "JSON response format schema is only supported with structuredOutputs",
144
- });
145
- }
146
-
147
- const {
148
- tools: openaiTools,
149
- toolChoice: openaiToolChoice,
150
- toolWarnings,
151
- } = prepareTools({
152
- tools,
153
- toolChoice,
154
- });
155
-
156
- return {
157
- args: {
158
- // model id:
159
- model: this.modelId,
160
-
161
- // model specific settings:
162
- user: compatibleOptions.user,
163
-
164
- // standardized settings:
165
- max_tokens: maxOutputTokens,
166
- temperature,
167
- top_p: topP,
168
- frequency_penalty: frequencyPenalty,
169
- presence_penalty: presencePenalty,
170
- response_format:
171
- responseFormat?.type === "json"
172
- ? this.supportsStructuredOutputs === true &&
173
- responseFormat.schema != null
174
- ? {
175
- type: "json_schema",
176
- json_schema: {
177
- schema: responseFormat.schema,
178
- name: responseFormat.name ?? "response",
179
- description: responseFormat.description,
180
- },
181
- }
182
- : { type: "json_object" }
183
- : undefined,
184
-
185
- stop: stopSequences,
186
- seed,
187
- ...Object.fromEntries(
188
- Object.entries(
189
- providerOptions?.[this.providerOptionsName] ?? {},
190
- ).filter(
191
- ([key]) =>
192
- !Object.keys(openaiCompatibleProviderOptions.shape).includes(key),
193
- ),
194
- ),
195
-
196
- reasoning_effort: compatibleOptions.reasoningEffort,
197
- verbosity: compatibleOptions.textVerbosity,
198
-
199
- // messages:
200
- messages: convertToOpenAICompatibleChatMessages(prompt),
201
-
202
- // tools:
203
- tools: openaiTools,
204
- tool_choice: openaiToolChoice,
205
-
206
- // thinking_budget for Claude models on Copilot
207
- thinking_budget: compatibleOptions.thinking_budget,
208
- },
209
- warnings: [...warnings, ...toolWarnings],
210
- };
211
- }
212
-
213
- async doGenerate(
214
- options: Parameters<LanguageModelV2["doGenerate"]>[0],
215
- ): Promise<Awaited<ReturnType<LanguageModelV2["doGenerate"]>>> {
216
- const { args, warnings } = await this.getArgs({ ...options });
217
-
218
- const body = JSON.stringify(args);
219
-
220
- const {
221
- responseHeaders,
222
- value: responseBody,
223
- rawValue: rawResponse,
224
- } = await postJsonToApi({
225
- url: this.config.url({
226
- path: "/chat/completions",
227
- modelId: this.modelId,
228
- }),
229
- headers: combineHeaders(this.config.headers(), options.headers),
230
- body: args,
231
- failedResponseHandler: this.failedResponseHandler,
232
- successfulResponseHandler: createJsonResponseHandler(
233
- OpenAICompatibleChatResponseSchema,
234
- ),
235
- abortSignal: options.abortSignal,
236
- fetch: this.config.fetch,
237
- });
238
-
239
- const choice = responseBody.choices[0];
240
- const content: Array<LanguageModelV2Content> = [];
241
-
242
- // text content:
243
- const text = choice.message.content;
244
- if (text != null && text.length > 0) {
245
- content.push({ type: "text", text });
246
- }
247
-
248
- // reasoning content (Copilot uses reasoning_text):
249
- const reasoning = choice.message.reasoning_text;
250
- if (reasoning != null && reasoning.length > 0) {
251
- content.push({
252
- type: "reasoning",
253
- text: reasoning,
254
- // Include reasoning_opaque for Copilot multi-turn reasoning
255
- providerMetadata: choice.message.reasoning_opaque
256
- ? { copilot: { reasoningOpaque: choice.message.reasoning_opaque } }
257
- : undefined,
258
- });
259
- }
260
-
261
- // tool calls:
262
- if (choice.message.tool_calls != null) {
263
- for (const toolCall of choice.message.tool_calls) {
264
- content.push({
265
- type: "tool-call",
266
- toolCallId: toolCall.id ?? generateId(),
267
- toolName: toolCall.function.name,
268
- input: toolCall.function.arguments!,
269
- });
270
- }
271
- }
272
-
273
- // provider metadata:
274
- const providerMetadata: SharedV2ProviderMetadata = {
275
- [this.providerOptionsName]: {},
276
- ...(await this.config.metadataExtractor?.extractMetadata?.({
277
- parsedBody: rawResponse,
278
- })),
279
- };
280
- const completionTokenDetails =
281
- responseBody.usage?.completion_tokens_details;
282
- if (completionTokenDetails?.accepted_prediction_tokens != null) {
283
- providerMetadata[this.providerOptionsName].acceptedPredictionTokens =
284
- completionTokenDetails?.accepted_prediction_tokens;
285
- }
286
- if (completionTokenDetails?.rejected_prediction_tokens != null) {
287
- providerMetadata[this.providerOptionsName].rejectedPredictionTokens =
288
- completionTokenDetails?.rejected_prediction_tokens;
289
- }
290
-
291
- return {
292
- content,
293
- finishReason: mapOpenAICompatibleFinishReason(choice.finish_reason),
294
- usage: {
295
- inputTokens: responseBody.usage?.prompt_tokens ?? undefined,
296
- outputTokens: responseBody.usage?.completion_tokens ?? undefined,
297
- totalTokens: responseBody.usage?.total_tokens ?? undefined,
298
- reasoningTokens:
299
- responseBody.usage?.completion_tokens_details?.reasoning_tokens ??
300
- undefined,
301
- cachedInputTokens:
302
- responseBody.usage?.prompt_tokens_details?.cached_tokens ?? undefined,
303
- },
304
- providerMetadata,
305
- request: { body },
306
- response: {
307
- ...getResponseMetadata(responseBody),
308
- headers: responseHeaders,
309
- body: rawResponse,
310
- },
311
- warnings,
312
- };
313
- }
314
-
315
- async doStream(
316
- options: Parameters<LanguageModelV2["doStream"]>[0],
317
- ): Promise<Awaited<ReturnType<LanguageModelV2["doStream"]>>> {
318
- const { args, warnings } = await this.getArgs({ ...options });
319
-
320
- const body = {
321
- ...args,
322
- stream: true,
323
-
324
- // only include stream_options when in strict compatibility mode:
325
- stream_options: this.config.includeUsage
326
- ? { include_usage: true }
327
- : undefined,
328
- };
329
-
330
- const metadataExtractor =
331
- this.config.metadataExtractor?.createStreamExtractor();
332
-
333
- const { responseHeaders, value: response } = await postJsonToApi({
334
- url: this.config.url({
335
- path: "/chat/completions",
336
- modelId: this.modelId,
337
- }),
338
- headers: combineHeaders(this.config.headers(), options.headers),
339
- body,
340
- failedResponseHandler: this.failedResponseHandler,
341
- successfulResponseHandler: createEventSourceResponseHandler(
342
- this.chunkSchema,
343
- ),
344
- abortSignal: options.abortSignal,
345
- fetch: this.config.fetch,
346
- });
347
-
348
- const toolCalls: Array<{
349
- id: string;
350
- type: "function";
351
- function: {
352
- name: string;
353
- arguments: string;
354
- };
355
- hasFinished: boolean;
356
- }> = [];
357
-
358
- let finishReason: LanguageModelV2FinishReason = "unknown";
359
- const usage: {
360
- completionTokens: number | undefined;
361
- completionTokensDetails: {
362
- reasoningTokens: number | undefined;
363
- acceptedPredictionTokens: number | undefined;
364
- rejectedPredictionTokens: number | undefined;
365
- };
366
- promptTokens: number | undefined;
367
- promptTokensDetails: {
368
- cachedTokens: number | undefined;
369
- };
370
- totalTokens: number | undefined;
371
- } = {
372
- completionTokens: undefined,
373
- completionTokensDetails: {
374
- reasoningTokens: undefined,
375
- acceptedPredictionTokens: undefined,
376
- rejectedPredictionTokens: undefined,
377
- },
378
- promptTokens: undefined,
379
- promptTokensDetails: {
380
- cachedTokens: undefined,
381
- },
382
- totalTokens: undefined,
383
- };
384
- let isFirstChunk = true;
385
- const providerOptionsName = this.providerOptionsName;
386
- let isActiveReasoning = false;
387
- let isActiveText = false;
388
- let reasoningOpaque: string | undefined;
389
-
390
- return {
391
- stream: response.pipeThrough(
392
- new TransformStream<
393
- ParseResult<z.infer<typeof this.chunkSchema>>,
394
- LanguageModelV2StreamPart
395
- >({
396
- start(controller) {
397
- controller.enqueue({ type: "stream-start", warnings });
398
- },
399
-
400
- // TODO we lost type safety on Chunk, most likely due to the error schema. MUST FIX
401
- transform(chunk, controller) {
402
- // Emit raw chunk if requested (before anything else)
403
- if (options.includeRawChunks) {
404
- controller.enqueue({ type: "raw", rawValue: chunk.rawValue });
405
- }
406
-
407
- // handle failed chunk parsing / validation:
408
- if (!chunk.success) {
409
- finishReason = "error";
410
- controller.enqueue({ type: "error", error: chunk.error });
411
- return;
412
- }
413
- const value = chunk.value;
414
-
415
- metadataExtractor?.processChunk(chunk.rawValue);
416
-
417
- // handle error chunks:
418
- if ("error" in value) {
419
- finishReason = "error";
420
- controller.enqueue({
421
- type: "error",
422
- error: (value as any).error.message,
423
- });
424
- return;
425
- }
426
-
427
- if (isFirstChunk) {
428
- isFirstChunk = false;
429
-
430
- controller.enqueue({
431
- type: "response-metadata",
432
- ...getResponseMetadata(value),
433
- });
434
- }
435
-
436
- if ((value as any).usage != null) {
437
- const {
438
- prompt_tokens,
439
- completion_tokens,
440
- total_tokens,
441
- prompt_tokens_details,
442
- completion_tokens_details,
443
- } = (value as any).usage;
444
-
445
- usage.promptTokens = prompt_tokens ?? undefined;
446
- usage.completionTokens = completion_tokens ?? undefined;
447
- usage.totalTokens = total_tokens ?? undefined;
448
- if (completion_tokens_details?.reasoning_tokens != null) {
449
- usage.completionTokensDetails.reasoningTokens =
450
- completion_tokens_details?.reasoning_tokens;
451
- }
452
- if (
453
- completion_tokens_details?.accepted_prediction_tokens != null
454
- ) {
455
- usage.completionTokensDetails.acceptedPredictionTokens =
456
- completion_tokens_details?.accepted_prediction_tokens;
457
- }
458
- if (
459
- completion_tokens_details?.rejected_prediction_tokens != null
460
- ) {
461
- usage.completionTokensDetails.rejectedPredictionTokens =
462
- completion_tokens_details?.rejected_prediction_tokens;
463
- }
464
- if (prompt_tokens_details?.cached_tokens != null) {
465
- usage.promptTokensDetails.cachedTokens =
466
- prompt_tokens_details?.cached_tokens;
467
- }
468
- }
469
-
470
- const choice = (value as any).choices[0];
471
-
472
- if (choice?.finish_reason != null) {
473
- finishReason = mapOpenAICompatibleFinishReason(
474
- choice.finish_reason,
475
- );
476
- }
477
-
478
- if (choice?.delta == null) {
479
- return;
480
- }
481
-
482
- const delta = choice.delta;
483
-
484
- // Capture reasoning_opaque for Copilot multi-turn reasoning
485
- // Claude models can send multiple reasoning_opaque delta chunks
486
- // during streaming (especially with parallel tool calls).
487
- // Concatenate them instead of throwing — same pattern as content/arguments.
488
- // See: https://github.com/anomalyco/opencode/issues/17011
489
- if (delta.reasoning_opaque) {
490
- reasoningOpaque =
491
- reasoningOpaque != null
492
- ? reasoningOpaque + delta.reasoning_opaque
493
- : delta.reasoning_opaque;
494
- }
495
-
496
- // enqueue reasoning before text deltas (Copilot uses reasoning_text):
497
- const reasoningContent = delta.reasoning_text;
498
- if (reasoningContent) {
499
- if (!isActiveReasoning) {
500
- controller.enqueue({
501
- type: "reasoning-start",
502
- id: "reasoning-0",
503
- });
504
- isActiveReasoning = true;
505
- }
506
-
507
- controller.enqueue({
508
- type: "reasoning-delta",
509
- id: "reasoning-0",
510
- delta: reasoningContent,
511
- });
512
- }
513
-
514
- if (delta.content) {
515
- // If reasoning was active and we're starting text, end reasoning first
516
- // This handles the case where reasoning_opaque and content come in the same chunk
517
- if (isActiveReasoning && !isActiveText) {
518
- controller.enqueue({
519
- type: "reasoning-end",
520
- id: "reasoning-0",
521
- providerMetadata: reasoningOpaque
522
- ? { copilot: { reasoningOpaque } }
523
- : undefined,
524
- });
525
- isActiveReasoning = false;
526
- }
527
-
528
- if (!isActiveText) {
529
- controller.enqueue({ type: "text-start", id: "txt-0" });
530
- isActiveText = true;
531
- }
532
-
533
- controller.enqueue({
534
- type: "text-delta",
535
- id: "txt-0",
536
- delta: delta.content,
537
- });
538
- }
539
-
540
- if (delta.tool_calls != null) {
541
- // If reasoning was active and we're starting tool calls, end reasoning first
542
- // This handles the case where reasoning goes directly to tool calls with no content
543
- if (isActiveReasoning) {
544
- controller.enqueue({
545
- type: "reasoning-end",
546
- id: "reasoning-0",
547
- providerMetadata: reasoningOpaque
548
- ? { copilot: { reasoningOpaque } }
549
- : undefined,
550
- });
551
- isActiveReasoning = false;
552
- }
553
- for (const toolCallDelta of delta.tool_calls) {
554
- const index = toolCallDelta.index;
555
-
556
- if (toolCalls[index] == null) {
557
- if (toolCallDelta.id == null) {
558
- throw new InvalidResponseDataError({
559
- data: toolCallDelta,
560
- message: `Expected 'id' to be a string.`,
561
- });
562
- }
563
-
564
- if (toolCallDelta.function?.name == null) {
565
- throw new InvalidResponseDataError({
566
- data: toolCallDelta,
567
- message: `Expected 'function.name' to be a string.`,
568
- });
569
- }
570
-
571
- controller.enqueue({
572
- type: "tool-input-start",
573
- id: toolCallDelta.id,
574
- toolName: toolCallDelta.function.name,
575
- });
576
-
577
- toolCalls[index] = {
578
- id: toolCallDelta.id,
579
- type: "function",
580
- function: {
581
- name: toolCallDelta.function.name,
582
- arguments: toolCallDelta.function.arguments ?? "",
583
- },
584
- hasFinished: false,
585
- };
586
-
587
- const toolCall = toolCalls[index];
588
-
589
- if (
590
- toolCall.function?.name != null &&
591
- toolCall.function?.arguments != null
592
- ) {
593
- // send delta if the argument text has already started:
594
- if (toolCall.function.arguments.length > 0) {
595
- controller.enqueue({
596
- type: "tool-input-delta",
597
- id: toolCall.id,
598
- delta: toolCall.function.arguments,
599
- });
600
- }
601
-
602
- // check if tool call is complete
603
- // (some providers send the full tool call in one chunk):
604
- if (isParsableJson(toolCall.function.arguments)) {
605
- controller.enqueue({
606
- type: "tool-input-end",
607
- id: toolCall.id,
608
- });
609
-
610
- controller.enqueue({
611
- type: "tool-call",
612
- toolCallId: toolCall.id ?? generateId(),
613
- toolName: toolCall.function.name,
614
- input: toolCall.function.arguments,
615
- });
616
- toolCall.hasFinished = true;
617
- }
618
- }
619
-
620
- continue;
621
- }
622
-
623
- // existing tool call, merge if not finished
624
- const toolCall = toolCalls[index];
625
-
626
- if (toolCall.hasFinished) {
627
- continue;
628
- }
629
-
630
- if (toolCallDelta.function?.arguments != null) {
631
- toolCall.function!.arguments +=
632
- toolCallDelta.function?.arguments ?? "";
633
- }
634
-
635
- // send delta
636
- controller.enqueue({
637
- type: "tool-input-delta",
638
- id: toolCall.id,
639
- delta: toolCallDelta.function.arguments ?? "",
640
- });
641
-
642
- // check if tool call is complete
643
- if (
644
- toolCall.function?.name != null &&
645
- toolCall.function?.arguments != null &&
646
- isParsableJson(toolCall.function.arguments)
647
- ) {
648
- controller.enqueue({
649
- type: "tool-input-end",
650
- id: toolCall.id,
651
- });
652
-
653
- controller.enqueue({
654
- type: "tool-call",
655
- toolCallId: toolCall.id ?? generateId(),
656
- toolName: toolCall.function.name,
657
- input: toolCall.function.arguments,
658
- });
659
- toolCall.hasFinished = true;
660
- }
661
- }
662
- }
663
- },
664
-
665
- flush(controller) {
666
- if (isActiveReasoning) {
667
- controller.enqueue({
668
- type: "reasoning-end",
669
- id: "reasoning-0",
670
- // Include reasoning_opaque for Copilot multi-turn reasoning
671
- providerMetadata: reasoningOpaque
672
- ? { copilot: { reasoningOpaque } }
673
- : undefined,
674
- });
675
- }
676
-
677
- if (isActiveText) {
678
- controller.enqueue({ type: "text-end", id: "txt-0" });
679
- }
680
-
681
- // go through all tool calls and send the ones that are not finished
682
- for (const toolCall of toolCalls.filter(
683
- (toolCall) => !toolCall.hasFinished,
684
- )) {
685
- controller.enqueue({
686
- type: "tool-input-end",
687
- id: toolCall.id,
688
- });
689
-
690
- controller.enqueue({
691
- type: "tool-call",
692
- toolCallId: toolCall.id ?? generateId(),
693
- toolName: toolCall.function.name,
694
- input: toolCall.function.arguments,
695
- });
696
- }
697
-
698
- const providerMetadata: SharedV2ProviderMetadata = {
699
- [providerOptionsName]: {},
700
- // Include reasoning_opaque for Copilot multi-turn reasoning
701
- ...(reasoningOpaque ? { copilot: { reasoningOpaque } } : {}),
702
- ...metadataExtractor?.buildMetadata(),
703
- };
704
- if (
705
- usage.completionTokensDetails.acceptedPredictionTokens != null
706
- ) {
707
- providerMetadata[providerOptionsName].acceptedPredictionTokens =
708
- usage.completionTokensDetails.acceptedPredictionTokens;
709
- }
710
- if (
711
- usage.completionTokensDetails.rejectedPredictionTokens != null
712
- ) {
713
- providerMetadata[providerOptionsName].rejectedPredictionTokens =
714
- usage.completionTokensDetails.rejectedPredictionTokens;
715
- }
716
-
717
- controller.enqueue({
718
- type: "finish",
719
- finishReason,
720
- usage: {
721
- inputTokens: usage.promptTokens ?? undefined,
722
- outputTokens: usage.completionTokens ?? undefined,
723
- totalTokens: usage.totalTokens ?? undefined,
724
- reasoningTokens:
725
- usage.completionTokensDetails.reasoningTokens ?? undefined,
726
- cachedInputTokens:
727
- usage.promptTokensDetails.cachedTokens ?? undefined,
728
- },
729
- providerMetadata,
730
- });
731
- },
732
- }),
733
- ),
734
- request: { body: JSON.stringify(body) },
735
- response: { headers: responseHeaders },
736
- };
737
- }
59
+ export class OpenAICompatibleChatLanguageModel implements LanguageModelV3 {
60
+ readonly specificationVersion = "v3";
61
+
62
+ readonly supportsStructuredOutputs: boolean;
63
+
64
+ readonly modelId: OpenAICompatibleChatModelId;
65
+ private readonly config: OpenAICompatibleChatConfig;
66
+ private readonly failedResponseHandler: ResponseHandler<APICallError>;
67
+ private readonly chunkSchema; // type inferred via constructor
68
+
69
+ constructor(modelId: OpenAICompatibleChatModelId, config: OpenAICompatibleChatConfig) {
70
+ this.modelId = modelId;
71
+ this.config = config;
72
+
73
+ // initialize error handling:
74
+ const errorStructure = config.errorStructure ?? defaultOpenAICompatibleErrorStructure;
75
+ this.chunkSchema = createOpenAICompatibleChatChunkSchema(errorStructure.errorSchema);
76
+ this.failedResponseHandler = createJsonErrorResponseHandler(errorStructure);
77
+
78
+ this.supportsStructuredOutputs = config.supportsStructuredOutputs ?? false;
79
+ }
80
+
81
+ get provider(): string {
82
+ return this.config.provider;
83
+ }
84
+
85
+ private get providerOptionsName(): string {
86
+ return this.config.provider.split(".")[0].trim();
87
+ }
88
+
89
+ get supportedUrls() {
90
+ return this.config.supportedUrls?.() ?? {};
91
+ }
92
+
93
+ private async getArgs({
94
+ prompt,
95
+ maxOutputTokens,
96
+ temperature,
97
+ topP,
98
+ topK,
99
+ frequencyPenalty,
100
+ presencePenalty,
101
+ providerOptions,
102
+ stopSequences,
103
+ responseFormat,
104
+ seed,
105
+ toolChoice,
106
+ tools,
107
+ }: LanguageModelV3CallOptions) {
108
+ const warnings: SharedV3Warning[] = [];
109
+
110
+ // Parse provider options
111
+ const compatibleOptions = Object.assign(
112
+ (await parseProviderOptions({
113
+ provider: "copilot",
114
+ providerOptions,
115
+ schema: openaiCompatibleProviderOptions,
116
+ })) ?? {},
117
+ (await parseProviderOptions({
118
+ provider: this.providerOptionsName,
119
+ providerOptions,
120
+ schema: openaiCompatibleProviderOptions,
121
+ })) ?? {},
122
+ );
123
+
124
+ if (topK != null) {
125
+ warnings.push({ type: "unsupported", feature: "topK" });
126
+ }
127
+
128
+ if (
129
+ responseFormat?.type === "json" &&
130
+ responseFormat.schema != null &&
131
+ !this.supportsStructuredOutputs
132
+ ) {
133
+ warnings.push({
134
+ type: "unsupported",
135
+ feature: "responseFormat",
136
+ details: "JSON response format schema is only supported with structuredOutputs",
137
+ });
138
+ }
139
+
140
+ const {
141
+ tools: openaiTools,
142
+ toolChoice: openaiToolChoice,
143
+ toolWarnings,
144
+ } = prepareTools({
145
+ tools,
146
+ toolChoice,
147
+ });
148
+
149
+ return {
150
+ args: {
151
+ // model id:
152
+ model: this.modelId,
153
+
154
+ // model specific settings:
155
+ user: compatibleOptions.user,
156
+
157
+ // standardized settings:
158
+ max_tokens: maxOutputTokens,
159
+ temperature,
160
+ top_p: topP,
161
+ frequency_penalty: frequencyPenalty,
162
+ presence_penalty: presencePenalty,
163
+ response_format:
164
+ responseFormat?.type === "json"
165
+ ? this.supportsStructuredOutputs === true && responseFormat.schema != null
166
+ ? {
167
+ type: "json_schema",
168
+ json_schema: {
169
+ schema: responseFormat.schema,
170
+ name: responseFormat.name ?? "response",
171
+ description: responseFormat.description,
172
+ },
173
+ }
174
+ : { type: "json_object" }
175
+ : undefined,
176
+
177
+ stop: stopSequences,
178
+ seed,
179
+ ...Object.fromEntries(
180
+ Object.entries(providerOptions?.[this.providerOptionsName] ?? {}).filter(
181
+ ([key]) => !Object.keys(openaiCompatibleProviderOptions.shape).includes(key),
182
+ ),
183
+ ),
184
+
185
+ reasoning_effort: compatibleOptions.reasoningEffort,
186
+ verbosity: compatibleOptions.textVerbosity,
187
+
188
+ // messages:
189
+ messages: convertToOpenAICompatibleChatMessages(prompt),
190
+
191
+ // tools:
192
+ tools: openaiTools,
193
+ tool_choice: openaiToolChoice,
194
+
195
+ // thinking_budget
196
+ thinking_budget: compatibleOptions.thinking_budget,
197
+ },
198
+ warnings: [...warnings, ...toolWarnings],
199
+ };
200
+ }
201
+
202
+ async doGenerate(options: LanguageModelV3CallOptions) {
203
+ const { args, warnings } = await this.getArgs({ ...options });
204
+
205
+ const body = JSON.stringify(args);
206
+
207
+ const {
208
+ responseHeaders,
209
+ value: responseBody,
210
+ rawValue: rawResponse,
211
+ } = await postJsonToApi({
212
+ url: this.config.url({
213
+ path: "/chat/completions",
214
+ modelId: this.modelId,
215
+ }),
216
+ headers: combineHeaders(this.config.headers(), options.headers),
217
+ body: args,
218
+ failedResponseHandler: this.failedResponseHandler,
219
+ successfulResponseHandler: createJsonResponseHandler(OpenAICompatibleChatResponseSchema),
220
+ abortSignal: options.abortSignal,
221
+ fetch: this.config.fetch,
222
+ });
223
+
224
+ const choice = responseBody.choices[0];
225
+ const content: Array<LanguageModelV3Content> = [];
226
+
227
+ // text content:
228
+ const text = choice.message.content;
229
+ if (text != null && text.length > 0) {
230
+ content.push({
231
+ type: "text",
232
+ text,
233
+ providerMetadata: choice.message.reasoning_opaque
234
+ ? { copilot: { reasoningOpaque: choice.message.reasoning_opaque } }
235
+ : undefined,
236
+ });
237
+ }
238
+
239
+ // reasoning content (Copilot uses reasoning_text):
240
+ const reasoning = choice.message.reasoning_text;
241
+ if (reasoning != null && reasoning.length > 0) {
242
+ content.push({
243
+ type: "reasoning",
244
+ text: reasoning,
245
+ // Include reasoning_opaque for Copilot multi-turn reasoning
246
+ providerMetadata: choice.message.reasoning_opaque
247
+ ? { copilot: { reasoningOpaque: choice.message.reasoning_opaque } }
248
+ : undefined,
249
+ });
250
+ }
251
+
252
+ // tool calls:
253
+ if (choice.message.tool_calls != null) {
254
+ for (const toolCall of choice.message.tool_calls) {
255
+ content.push({
256
+ type: "tool-call",
257
+ toolCallId: toolCall.id ?? generateId(),
258
+ toolName: toolCall.function.name,
259
+ input: toolCall.function.arguments!,
260
+ providerMetadata: choice.message.reasoning_opaque
261
+ ? { copilot: { reasoningOpaque: choice.message.reasoning_opaque } }
262
+ : undefined,
263
+ });
264
+ }
265
+ }
266
+
267
+ // provider metadata:
268
+ const providerMetadata: SharedV3ProviderMetadata = {
269
+ [this.providerOptionsName]: {},
270
+ ...(await this.config.metadataExtractor?.extractMetadata?.({
271
+ parsedBody: rawResponse,
272
+ })),
273
+ };
274
+ const completionTokenDetails = responseBody.usage?.completion_tokens_details;
275
+ if (completionTokenDetails?.accepted_prediction_tokens != null) {
276
+ providerMetadata[this.providerOptionsName].acceptedPredictionTokens =
277
+ completionTokenDetails?.accepted_prediction_tokens;
278
+ }
279
+ if (completionTokenDetails?.rejected_prediction_tokens != null) {
280
+ providerMetadata[this.providerOptionsName].rejectedPredictionTokens =
281
+ completionTokenDetails?.rejected_prediction_tokens;
282
+ }
283
+
284
+ return {
285
+ content,
286
+ finishReason: {
287
+ unified: mapOpenAICompatibleFinishReason(choice.finish_reason),
288
+ raw: choice.finish_reason ?? undefined,
289
+ },
290
+ usage: {
291
+ inputTokens: {
292
+ total: responseBody.usage?.prompt_tokens ?? undefined,
293
+ noCache: undefined,
294
+ cacheRead: responseBody.usage?.prompt_tokens_details?.cached_tokens ?? undefined,
295
+ cacheWrite: undefined,
296
+ },
297
+ outputTokens: {
298
+ total: responseBody.usage?.completion_tokens ?? undefined,
299
+ text: undefined,
300
+ reasoning: responseBody.usage?.completion_tokens_details?.reasoning_tokens ?? undefined,
301
+ },
302
+ raw: responseBody.usage ?? undefined,
303
+ },
304
+ providerMetadata,
305
+ request: { body },
306
+ response: {
307
+ ...getResponseMetadata(responseBody),
308
+ headers: responseHeaders,
309
+ body: rawResponse,
310
+ },
311
+ warnings,
312
+ };
313
+ }
314
+
315
+ async doStream(options: LanguageModelV3CallOptions) {
316
+ const { args, warnings } = await this.getArgs({ ...options });
317
+
318
+ const body = {
319
+ ...args,
320
+ stream: true,
321
+
322
+ // only include stream_options when in strict compatibility mode:
323
+ stream_options: this.config.includeUsage ? { include_usage: true } : undefined,
324
+ };
325
+
326
+ const metadataExtractor = this.config.metadataExtractor?.createStreamExtractor();
327
+
328
+ const { responseHeaders, value: response } = await postJsonToApi({
329
+ url: this.config.url({
330
+ path: "/chat/completions",
331
+ modelId: this.modelId,
332
+ }),
333
+ headers: combineHeaders(this.config.headers(), options.headers),
334
+ body,
335
+ failedResponseHandler: this.failedResponseHandler,
336
+ successfulResponseHandler: createEventSourceResponseHandler(this.chunkSchema),
337
+ abortSignal: options.abortSignal,
338
+ fetch: this.config.fetch,
339
+ });
340
+
341
+ const toolCalls: Array<{
342
+ id: string;
343
+ type: "function";
344
+ function: {
345
+ name: string;
346
+ arguments: string;
347
+ };
348
+ hasFinished: boolean;
349
+ }> = [];
350
+
351
+ let finishReason: {
352
+ unified: ReturnType<typeof mapOpenAICompatibleFinishReason>;
353
+ raw: string | undefined;
354
+ } = {
355
+ unified: "other",
356
+ raw: undefined,
357
+ };
358
+ const usage: {
359
+ completionTokens: number | undefined;
360
+ completionTokensDetails: {
361
+ reasoningTokens: number | undefined;
362
+ acceptedPredictionTokens: number | undefined;
363
+ rejectedPredictionTokens: number | undefined;
364
+ };
365
+ promptTokens: number | undefined;
366
+ promptTokensDetails: {
367
+ cachedTokens: number | undefined;
368
+ };
369
+ totalTokens: number | undefined;
370
+ } = {
371
+ completionTokens: undefined,
372
+ completionTokensDetails: {
373
+ reasoningTokens: undefined,
374
+ acceptedPredictionTokens: undefined,
375
+ rejectedPredictionTokens: undefined,
376
+ },
377
+ promptTokens: undefined,
378
+ promptTokensDetails: {
379
+ cachedTokens: undefined,
380
+ },
381
+ totalTokens: undefined,
382
+ };
383
+ let isFirstChunk = true;
384
+ const providerOptionsName = this.providerOptionsName;
385
+ let isActiveReasoning = false;
386
+ let isActiveText = false;
387
+ let reasoningOpaque: string | undefined;
388
+
389
+ return {
390
+ stream: response.pipeThrough(
391
+ new TransformStream<
392
+ ParseResult<z.infer<typeof this.chunkSchema>>,
393
+ LanguageModelV3StreamPart
394
+ >({
395
+ start(controller) {
396
+ controller.enqueue({ type: "stream-start", warnings });
397
+ },
398
+
399
+ // TODO we lost type safety on Chunk, most likely due to the error schema. MUST FIX
400
+ transform(chunk, controller) {
401
+ // Emit raw chunk if requested (before anything else)
402
+ if (options.includeRawChunks) {
403
+ controller.enqueue({ type: "raw", rawValue: chunk.rawValue });
404
+ }
405
+
406
+ // handle failed chunk parsing / validation:
407
+ if (!chunk.success) {
408
+ finishReason = {
409
+ unified: "error",
410
+ raw: undefined,
411
+ };
412
+ controller.enqueue({ type: "error", error: chunk.error });
413
+ return;
414
+ }
415
+ const value = chunk.value;
416
+
417
+ metadataExtractor?.processChunk(chunk.rawValue);
418
+
419
+ // handle error chunks:
420
+ if ("error" in value) {
421
+ finishReason = {
422
+ unified: "error",
423
+ raw: undefined,
424
+ };
425
+ controller.enqueue({ type: "error", error: value.error.message });
426
+ return;
427
+ }
428
+
429
+ if (isFirstChunk) {
430
+ isFirstChunk = false;
431
+
432
+ controller.enqueue({
433
+ type: "response-metadata",
434
+ ...getResponseMetadata(value),
435
+ });
436
+ }
437
+
438
+ if (value.usage != null) {
439
+ const {
440
+ prompt_tokens,
441
+ completion_tokens,
442
+ total_tokens,
443
+ prompt_tokens_details,
444
+ completion_tokens_details,
445
+ } = value.usage;
446
+
447
+ usage.promptTokens = prompt_tokens ?? undefined;
448
+ usage.completionTokens = completion_tokens ?? undefined;
449
+ usage.totalTokens = total_tokens ?? undefined;
450
+ if (completion_tokens_details?.reasoning_tokens != null) {
451
+ usage.completionTokensDetails.reasoningTokens =
452
+ completion_tokens_details?.reasoning_tokens;
453
+ }
454
+ if (completion_tokens_details?.accepted_prediction_tokens != null) {
455
+ usage.completionTokensDetails.acceptedPredictionTokens =
456
+ completion_tokens_details?.accepted_prediction_tokens;
457
+ }
458
+ if (completion_tokens_details?.rejected_prediction_tokens != null) {
459
+ usage.completionTokensDetails.rejectedPredictionTokens =
460
+ completion_tokens_details?.rejected_prediction_tokens;
461
+ }
462
+ if (prompt_tokens_details?.cached_tokens != null) {
463
+ usage.promptTokensDetails.cachedTokens = prompt_tokens_details?.cached_tokens;
464
+ }
465
+ }
466
+
467
+ const choice = value.choices[0];
468
+
469
+ if (choice?.finish_reason != null) {
470
+ finishReason = {
471
+ unified: mapOpenAICompatibleFinishReason(choice.finish_reason),
472
+ raw: choice.finish_reason ?? undefined,
473
+ };
474
+ }
475
+
476
+ if (choice?.delta == null) {
477
+ return;
478
+ }
479
+
480
+ const delta = choice.delta;
481
+
482
+ // Capture reasoning_opaque for Copilot multi-turn reasoning.
483
+ // Claude models can send multiple reasoning_opaque delta chunks
484
+ // during streaming (especially with parallel tool calls).
485
+ // Concatenate them instead of throwing same pattern as content/arguments.
486
+ if (delta.reasoning_opaque) {
487
+ reasoningOpaque =
488
+ reasoningOpaque != null
489
+ ? reasoningOpaque + delta.reasoning_opaque
490
+ : delta.reasoning_opaque;
491
+ }
492
+
493
+ // enqueue reasoning before text deltas (Copilot uses reasoning_text):
494
+ const reasoningContent = delta.reasoning_text;
495
+ if (reasoningContent) {
496
+ if (!isActiveReasoning) {
497
+ controller.enqueue({
498
+ type: "reasoning-start",
499
+ id: "reasoning-0",
500
+ });
501
+ isActiveReasoning = true;
502
+ }
503
+
504
+ controller.enqueue({
505
+ type: "reasoning-delta",
506
+ id: "reasoning-0",
507
+ delta: reasoningContent,
508
+ });
509
+ }
510
+
511
+ if (delta.content) {
512
+ // If reasoning was active and we're starting text, end reasoning first
513
+ // This handles the case where reasoning_opaque and content come in the same chunk
514
+ if (isActiveReasoning && !isActiveText) {
515
+ controller.enqueue({
516
+ type: "reasoning-end",
517
+ id: "reasoning-0",
518
+ providerMetadata: reasoningOpaque ? { copilot: { reasoningOpaque } } : undefined,
519
+ });
520
+ isActiveReasoning = false;
521
+ }
522
+
523
+ if (!isActiveText) {
524
+ controller.enqueue({
525
+ type: "text-start",
526
+ id: "txt-0",
527
+ providerMetadata: reasoningOpaque ? { copilot: { reasoningOpaque } } : undefined,
528
+ });
529
+ isActiveText = true;
530
+ }
531
+
532
+ controller.enqueue({
533
+ type: "text-delta",
534
+ id: "txt-0",
535
+ delta: delta.content,
536
+ });
537
+ }
538
+
539
+ if (delta.tool_calls != null) {
540
+ // If reasoning was active and we're starting tool calls, end reasoning first
541
+ // This handles the case where reasoning goes directly to tool calls with no content
542
+ if (isActiveReasoning) {
543
+ controller.enqueue({
544
+ type: "reasoning-end",
545
+ id: "reasoning-0",
546
+ providerMetadata: reasoningOpaque ? { copilot: { reasoningOpaque } } : undefined,
547
+ });
548
+ isActiveReasoning = false;
549
+ }
550
+ for (const toolCallDelta of delta.tool_calls) {
551
+ const index = toolCallDelta.index;
552
+
553
+ if (toolCalls[index] == null) {
554
+ if (toolCallDelta.id == null) {
555
+ throw new InvalidResponseDataError({
556
+ data: toolCallDelta,
557
+ message: `Expected 'id' to be a string.`,
558
+ });
559
+ }
560
+
561
+ if (toolCallDelta.function?.name == null) {
562
+ throw new InvalidResponseDataError({
563
+ data: toolCallDelta,
564
+ message: `Expected 'function.name' to be a string.`,
565
+ });
566
+ }
567
+
568
+ controller.enqueue({
569
+ type: "tool-input-start",
570
+ id: toolCallDelta.id,
571
+ toolName: toolCallDelta.function.name,
572
+ });
573
+
574
+ toolCalls[index] = {
575
+ id: toolCallDelta.id,
576
+ type: "function",
577
+ function: {
578
+ name: toolCallDelta.function.name,
579
+ arguments: toolCallDelta.function.arguments ?? "",
580
+ },
581
+ hasFinished: false,
582
+ };
583
+
584
+ const toolCall = toolCalls[index];
585
+
586
+ if (toolCall.function?.name != null && toolCall.function?.arguments != null) {
587
+ // send delta if the argument text has already started:
588
+ if (toolCall.function.arguments.length > 0) {
589
+ controller.enqueue({
590
+ type: "tool-input-delta",
591
+ id: toolCall.id,
592
+ delta: toolCall.function.arguments,
593
+ });
594
+ }
595
+
596
+ // check if tool call is complete
597
+ // (some providers send the full tool call in one chunk):
598
+ if (isParsableJson(toolCall.function.arguments)) {
599
+ controller.enqueue({
600
+ type: "tool-input-end",
601
+ id: toolCall.id,
602
+ });
603
+
604
+ controller.enqueue({
605
+ type: "tool-call",
606
+ toolCallId: toolCall.id ?? generateId(),
607
+ toolName: toolCall.function.name,
608
+ input: toolCall.function.arguments,
609
+ providerMetadata: reasoningOpaque
610
+ ? { copilot: { reasoningOpaque } }
611
+ : undefined,
612
+ });
613
+ toolCall.hasFinished = true;
614
+ }
615
+ }
616
+
617
+ continue;
618
+ }
619
+
620
+ // existing tool call, merge if not finished
621
+ const toolCall = toolCalls[index];
622
+
623
+ if (toolCall.hasFinished) {
624
+ continue;
625
+ }
626
+
627
+ if (toolCallDelta.function?.arguments != null) {
628
+ toolCall.function!.arguments += toolCallDelta.function?.arguments ?? "";
629
+ }
630
+
631
+ // send delta
632
+ controller.enqueue({
633
+ type: "tool-input-delta",
634
+ id: toolCall.id,
635
+ delta: toolCallDelta.function.arguments ?? "",
636
+ });
637
+
638
+ // check if tool call is complete
639
+ if (
640
+ toolCall.function?.name != null &&
641
+ toolCall.function?.arguments != null &&
642
+ isParsableJson(toolCall.function.arguments)
643
+ ) {
644
+ controller.enqueue({
645
+ type: "tool-input-end",
646
+ id: toolCall.id,
647
+ });
648
+
649
+ controller.enqueue({
650
+ type: "tool-call",
651
+ toolCallId: toolCall.id ?? generateId(),
652
+ toolName: toolCall.function.name,
653
+ input: toolCall.function.arguments,
654
+ providerMetadata: reasoningOpaque
655
+ ? { copilot: { reasoningOpaque } }
656
+ : undefined,
657
+ });
658
+ toolCall.hasFinished = true;
659
+ }
660
+ }
661
+ }
662
+ },
663
+
664
+ flush(controller) {
665
+ if (isActiveReasoning) {
666
+ controller.enqueue({
667
+ type: "reasoning-end",
668
+ id: "reasoning-0",
669
+ // Include reasoning_opaque for Copilot multi-turn reasoning
670
+ providerMetadata: reasoningOpaque ? { copilot: { reasoningOpaque } } : undefined,
671
+ });
672
+ }
673
+
674
+ if (isActiveText) {
675
+ controller.enqueue({ type: "text-end", id: "txt-0" });
676
+ }
677
+
678
+ // go through all tool calls and send the ones that are not finished
679
+ for (const toolCall of toolCalls.filter((toolCall) => !toolCall.hasFinished)) {
680
+ controller.enqueue({
681
+ type: "tool-input-end",
682
+ id: toolCall.id,
683
+ });
684
+
685
+ controller.enqueue({
686
+ type: "tool-call",
687
+ toolCallId: toolCall.id ?? generateId(),
688
+ toolName: toolCall.function.name,
689
+ input: toolCall.function.arguments,
690
+ });
691
+ }
692
+
693
+ const providerMetadata: SharedV3ProviderMetadata = {
694
+ [providerOptionsName]: {},
695
+ // Include reasoning_opaque for Copilot multi-turn reasoning
696
+ ...(reasoningOpaque ? { copilot: { reasoningOpaque } } : {}),
697
+ ...metadataExtractor?.buildMetadata(),
698
+ };
699
+ if (usage.completionTokensDetails.acceptedPredictionTokens != null) {
700
+ providerMetadata[providerOptionsName].acceptedPredictionTokens =
701
+ usage.completionTokensDetails.acceptedPredictionTokens;
702
+ }
703
+ if (usage.completionTokensDetails.rejectedPredictionTokens != null) {
704
+ providerMetadata[providerOptionsName].rejectedPredictionTokens =
705
+ usage.completionTokensDetails.rejectedPredictionTokens;
706
+ }
707
+
708
+ controller.enqueue({
709
+ type: "finish",
710
+ finishReason,
711
+ usage: {
712
+ inputTokens: {
713
+ total: usage.promptTokens,
714
+ noCache:
715
+ usage.promptTokens != undefined &&
716
+ usage.promptTokensDetails.cachedTokens != undefined
717
+ ? usage.promptTokens - usage.promptTokensDetails.cachedTokens
718
+ : undefined,
719
+ cacheRead: usage.promptTokensDetails.cachedTokens,
720
+ cacheWrite: undefined,
721
+ },
722
+ outputTokens: {
723
+ total: usage.completionTokens,
724
+ text: undefined,
725
+ reasoning: usage.completionTokensDetails.reasoningTokens,
726
+ },
727
+ raw: {
728
+ prompt_tokens: usage.promptTokens ?? null,
729
+ completion_tokens: usage.completionTokens ?? null,
730
+ total_tokens: usage.totalTokens ?? null,
731
+ },
732
+ },
733
+ providerMetadata,
734
+ });
735
+ },
736
+ }),
737
+ ),
738
+ request: { body },
739
+ response: { headers: responseHeaders },
740
+ };
741
+ }
738
742
  }
739
743
 
740
744
  const openaiCompatibleTokenUsageSchema = z
741
- .object({
742
- prompt_tokens: z.number().nullish(),
743
- completion_tokens: z.number().nullish(),
744
- total_tokens: z.number().nullish(),
745
- prompt_tokens_details: z
746
- .object({
747
- cached_tokens: z.number().nullish(),
748
- })
749
- .nullish(),
750
- completion_tokens_details: z
751
- .object({
752
- reasoning_tokens: z.number().nullish(),
753
- accepted_prediction_tokens: z.number().nullish(),
754
- rejected_prediction_tokens: z.number().nullish(),
755
- })
756
- .nullish(),
757
- })
758
- .nullish();
745
+ .object({
746
+ prompt_tokens: z.number().nullish(),
747
+ completion_tokens: z.number().nullish(),
748
+ total_tokens: z.number().nullish(),
749
+ prompt_tokens_details: z
750
+ .object({
751
+ cached_tokens: z.number().nullish(),
752
+ })
753
+ .nullish(),
754
+ completion_tokens_details: z
755
+ .object({
756
+ reasoning_tokens: z.number().nullish(),
757
+ accepted_prediction_tokens: z.number().nullish(),
758
+ rejected_prediction_tokens: z.number().nullish(),
759
+ })
760
+ .nullish(),
761
+ })
762
+ .nullish();
759
763
 
760
764
  // limited version of the schema, focussed on what is needed for the implementation
761
765
  // this approach limits breakages when the API changes and increases efficiency
762
766
  const OpenAICompatibleChatResponseSchema = z.object({
763
- id: z.string().nullish(),
764
- created: z.number().nullish(),
765
- model: z.string().nullish(),
766
- choices: z.array(
767
- z.object({
768
- message: z.object({
769
- role: z.literal("assistant").nullish(),
770
- content: z.string().nullish(),
771
- // Copilot-specific reasoning fields
772
- reasoning_text: z.string().nullish(),
773
- reasoning_opaque: z.string().nullish(),
774
- tool_calls: z
775
- .array(
776
- z.object({
777
- id: z.string().nullish(),
778
- function: z.object({
779
- name: z.string(),
780
- arguments: z.string(),
781
- }),
782
- }),
783
- )
784
- .nullish(),
785
- }),
786
- finish_reason: z.string().nullish(),
787
- }),
788
- ),
789
- usage: openaiCompatibleTokenUsageSchema,
767
+ id: z.string().nullish(),
768
+ created: z.number().nullish(),
769
+ model: z.string().nullish(),
770
+ choices: z.array(
771
+ z.object({
772
+ message: z.object({
773
+ role: z.literal("assistant").nullish(),
774
+ content: z.string().nullish(),
775
+ // Copilot-specific reasoning fields
776
+ reasoning_text: z.string().nullish(),
777
+ reasoning_opaque: z.string().nullish(),
778
+ tool_calls: z
779
+ .array(
780
+ z.object({
781
+ id: z.string().nullish(),
782
+ function: z.object({
783
+ name: z.string(),
784
+ arguments: z.string(),
785
+ }),
786
+ }),
787
+ )
788
+ .nullish(),
789
+ }),
790
+ finish_reason: z.string().nullish(),
791
+ }),
792
+ ),
793
+ usage: openaiCompatibleTokenUsageSchema,
790
794
  });
791
795
 
792
796
  // limited version of the schema, focussed on what is needed for the implementation
793
797
  // this approach limits breakages when the API changes and increases efficiency
794
- const createOpenAICompatibleChatChunkSchema = <
795
- ERROR_SCHEMA extends z.core.$ZodType,
796
- >(
797
- errorSchema: ERROR_SCHEMA,
798
+ const createOpenAICompatibleChatChunkSchema = <ERROR_SCHEMA extends z.core.$ZodType>(
799
+ errorSchema: ERROR_SCHEMA,
798
800
  ) =>
799
- z.union([
800
- z.object({
801
- id: z.string().nullish(),
802
- created: z.number().nullish(),
803
- model: z.string().nullish(),
804
- choices: z.array(
805
- z.object({
806
- delta: z
807
- .object({
808
- role: z.enum(["assistant"]).nullish(),
809
- content: z.string().nullish(),
810
- // Copilot-specific reasoning fields
811
- reasoning_text: z.string().nullish(),
812
- reasoning_opaque: z.string().nullish(),
813
- tool_calls: z
814
- .array(
815
- z.object({
816
- index: z.number(),
817
- id: z.string().nullish(),
818
- function: z.object({
819
- name: z.string().nullish(),
820
- arguments: z.string().nullish(),
821
- }),
822
- }),
823
- )
824
- .nullish(),
825
- })
826
- .nullish(),
827
- finish_reason: z.string().nullish(),
828
- }),
829
- ),
830
- usage: openaiCompatibleTokenUsageSchema,
831
- }),
832
- errorSchema,
833
- ]);
801
+ z.union([
802
+ z.object({
803
+ id: z.string().nullish(),
804
+ created: z.number().nullish(),
805
+ model: z.string().nullish(),
806
+ choices: z.array(
807
+ z.object({
808
+ delta: z
809
+ .object({
810
+ role: z.enum(["assistant"]).nullish(),
811
+ content: z.string().nullish(),
812
+ // Copilot-specific reasoning fields
813
+ reasoning_text: z.string().nullish(),
814
+ reasoning_opaque: z.string().nullish(),
815
+ tool_calls: z
816
+ .array(
817
+ z.object({
818
+ index: z.number(),
819
+ id: z.string().nullish(),
820
+ function: z.object({
821
+ name: z.string().nullish(),
822
+ arguments: z.string().nullish(),
823
+ }),
824
+ }),
825
+ )
826
+ .nullish(),
827
+ })
828
+ .nullish(),
829
+ finish_reason: z.string().nullish(),
830
+ }),
831
+ ),
832
+ usage: openaiCompatibleTokenUsageSchema,
833
+ }),
834
+ errorSchema,
835
+ ]);