@oh-my-pi/pi-ai 6.8.2 → 6.8.4
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.
- package/README.md +31 -0
- package/package.json +2 -2
- package/src/providers/amazon-bedrock.ts +4 -2
- package/src/providers/anthropic.ts +168 -102
- package/src/providers/cursor.ts +2 -0
- package/src/providers/google-gemini-cli.ts +2 -0
- package/src/providers/google-vertex.ts +1 -0
- package/src/providers/google.ts +1 -0
- package/src/providers/openai-codex-responses.ts +35 -17
- package/src/providers/openai-completions.ts +9 -3
- package/src/providers/openai-responses.ts +44 -8
- package/src/providers/transform-messages.ts +59 -7
- package/src/stream.ts +15 -6
- package/src/types.ts +10 -0
- package/src/utils/oauth/callback-server.ts +2 -2
- package/src/utils/oauth/index.ts +1 -0
- package/src/utils/oauth/openai-codex.ts +10 -3
package/README.md
CHANGED
|
@@ -607,6 +607,28 @@ context.messages.push({ role: "user", content: "Please continue" });
|
|
|
607
607
|
const continuation = await complete(model, context);
|
|
608
608
|
```
|
|
609
609
|
|
|
610
|
+
### Common Stream Options
|
|
611
|
+
|
|
612
|
+
All providers accept the base `StreamOptions` (in addition to provider-specific options):
|
|
613
|
+
|
|
614
|
+
- `apiKey`: Override the provider API key
|
|
615
|
+
- `headers`: Extra request headers merged on top of model-defined headers
|
|
616
|
+
- `sessionId`: Provider-specific session identifier (prompt caching/routing)
|
|
617
|
+
- `signal`: Abort in-flight requests
|
|
618
|
+
- `onPayload`: Callback invoked with the provider request payload just before sending
|
|
619
|
+
|
|
620
|
+
Example:
|
|
621
|
+
|
|
622
|
+
```typescript
|
|
623
|
+
const response = await complete(model, context, {
|
|
624
|
+
apiKey: "sk-live",
|
|
625
|
+
headers: { "X-Debug-Trace": "true" },
|
|
626
|
+
onPayload: (payload) => {
|
|
627
|
+
console.log("request payload", payload);
|
|
628
|
+
},
|
|
629
|
+
});
|
|
630
|
+
```
|
|
631
|
+
|
|
610
632
|
## APIs, Models, and Providers
|
|
611
633
|
|
|
612
634
|
The library implements 4 API interfaces, each with its own streaming function and options:
|
|
@@ -987,6 +1009,15 @@ import {
|
|
|
987
1009
|
} from "@oh-my-pi/pi-ai";
|
|
988
1010
|
```
|
|
989
1011
|
|
|
1012
|
+
`loginOpenAICodex` accepts an optional `originator` value used in the OAuth flow:
|
|
1013
|
+
|
|
1014
|
+
```typescript
|
|
1015
|
+
await loginOpenAICodex({
|
|
1016
|
+
onAuth: ({ url }) => console.log(url),
|
|
1017
|
+
originator: "my-cli",
|
|
1018
|
+
});
|
|
1019
|
+
```
|
|
1020
|
+
|
|
990
1021
|
### Login Flow Example
|
|
991
1022
|
|
|
992
1023
|
```typescript
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oh-my-pi/pi-ai",
|
|
3
|
-
"version": "6.8.
|
|
3
|
+
"version": "6.8.4",
|
|
4
4
|
"description": "Unified LLM API with automatic model discovery and provider configuration",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"test": "bun test"
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@oh-my-pi/pi-utils": "6.8.
|
|
20
|
+
"@oh-my-pi/pi-utils": "6.8.4",
|
|
21
21
|
"@anthropic-ai/sdk": "0.71.2",
|
|
22
22
|
"@aws-sdk/client-bedrock-runtime": "^3.968.0",
|
|
23
23
|
"@bufbuild/protobuf": "^2.10.2",
|
|
@@ -93,14 +93,16 @@ export const streamBedrock: StreamFunction<"bedrock-converse-stream"> = (
|
|
|
93
93
|
profile: options.profile,
|
|
94
94
|
});
|
|
95
95
|
|
|
96
|
-
const
|
|
96
|
+
const commandInput = {
|
|
97
97
|
modelId: model.id,
|
|
98
98
|
messages: convertMessages(context, model),
|
|
99
99
|
system: buildSystemPrompt(context.systemPrompt, model),
|
|
100
100
|
inferenceConfig: { maxTokens: options.maxTokens, temperature: options.temperature },
|
|
101
101
|
toolConfig: convertToolConfig(context.tools, options.toolChoice),
|
|
102
102
|
additionalModelRequestFields: buildAdditionalModelRequestFields(model, options),
|
|
103
|
-
}
|
|
103
|
+
};
|
|
104
|
+
options?.onPayload?.(commandInput);
|
|
105
|
+
const command = new ConverseStreamCommand(commandInput);
|
|
104
106
|
|
|
105
107
|
const response = await client.send(command, { abortSignal: options.signal });
|
|
106
108
|
|
|
@@ -161,8 +161,9 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
|
|
|
161
161
|
try {
|
|
162
162
|
const apiKey = options?.apiKey ?? getEnvApiKey(model.provider) ?? "";
|
|
163
163
|
const extraBetas = normalizeExtraBetas(options?.betas);
|
|
164
|
-
const { client, isOAuthToken } = createClient(model, apiKey, extraBetas, true);
|
|
164
|
+
const { client, isOAuthToken } = createClient(model, apiKey, extraBetas, true, options?.headers);
|
|
165
165
|
const params = buildParams(model, context, isOAuthToken, options);
|
|
166
|
+
options?.onPayload?.(params);
|
|
166
167
|
const anthropicStream = client.messages.stream({ ...params, stream: true }, { signal: options?.signal });
|
|
167
168
|
stream.push({ type: "start", partial: output });
|
|
168
169
|
|
|
@@ -291,11 +292,21 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
|
|
|
291
292
|
if (event.delta.stop_reason) {
|
|
292
293
|
output.stopReason = mapStopReason(event.delta.stop_reason);
|
|
293
294
|
}
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
295
|
+
// message_delta.usage only contains output_tokens (cumulative), not input_tokens
|
|
296
|
+
// Preserve input token counts from message_start, only update output
|
|
297
|
+
if (event.usage.output_tokens !== undefined && event.usage.output_tokens !== null) {
|
|
298
|
+
output.usage.output = event.usage.output_tokens;
|
|
299
|
+
}
|
|
300
|
+
// These fields may or may not be present in message_delta
|
|
301
|
+
if (event.usage.cache_read_input_tokens !== undefined && event.usage.cache_read_input_tokens !== null) {
|
|
302
|
+
output.usage.cacheRead = event.usage.cache_read_input_tokens;
|
|
303
|
+
}
|
|
304
|
+
if (
|
|
305
|
+
event.usage.cache_creation_input_tokens !== undefined &&
|
|
306
|
+
event.usage.cache_creation_input_tokens !== null
|
|
307
|
+
) {
|
|
308
|
+
output.usage.cacheWrite = event.usage.cache_creation_input_tokens;
|
|
309
|
+
}
|
|
299
310
|
output.usage.totalTokens =
|
|
300
311
|
output.usage.input + output.usage.output + output.usage.cacheRead + output.usage.cacheWrite;
|
|
301
312
|
calculateCost(model, output.usage);
|
|
@@ -420,6 +431,7 @@ function createClient(
|
|
|
420
431
|
apiKey: string,
|
|
421
432
|
extraBetas: string[],
|
|
422
433
|
stream: boolean,
|
|
434
|
+
extraHeaders?: Record<string, string>,
|
|
423
435
|
): { client: Anthropic; isOAuthToken: boolean } {
|
|
424
436
|
const oauthToken = isOAuthToken(apiKey);
|
|
425
437
|
|
|
@@ -438,7 +450,7 @@ function createClient(
|
|
|
438
450
|
isOAuth: oauthToken,
|
|
439
451
|
extraBetas: mergedBetas,
|
|
440
452
|
stream,
|
|
441
|
-
modelHeaders: model.headers,
|
|
453
|
+
modelHeaders: { ...(model.headers ?? {}), ...(extraHeaders ?? {}) },
|
|
442
454
|
});
|
|
443
455
|
|
|
444
456
|
const clientOptions: ConstructorParameters<typeof Anthropic>[0] = {
|
|
@@ -466,16 +478,13 @@ export type AnthropicSystemBlock = {
|
|
|
466
478
|
};
|
|
467
479
|
|
|
468
480
|
type CacheControlBlock = {
|
|
469
|
-
cache_control?: { type: "ephemeral" };
|
|
481
|
+
cache_control?: { type: "ephemeral" } | null;
|
|
470
482
|
};
|
|
471
483
|
|
|
472
|
-
type CacheControlMode = "none" | "toolBlocks" | "userText";
|
|
473
|
-
|
|
474
484
|
const cacheControlEphemeral = { type: "ephemeral" as const };
|
|
475
485
|
|
|
476
486
|
type SystemBlockOptions = {
|
|
477
487
|
includeClaudeCodeInstruction?: boolean;
|
|
478
|
-
includeCacheControl?: boolean;
|
|
479
488
|
extraInstructions?: string[];
|
|
480
489
|
};
|
|
481
490
|
|
|
@@ -483,17 +492,15 @@ export function buildAnthropicSystemBlocks(
|
|
|
483
492
|
systemPrompt: string | undefined,
|
|
484
493
|
options: SystemBlockOptions = {},
|
|
485
494
|
): AnthropicSystemBlock[] | undefined {
|
|
486
|
-
const { includeClaudeCodeInstruction = false,
|
|
495
|
+
const { includeClaudeCodeInstruction = false, extraInstructions = [] } = options;
|
|
487
496
|
const blocks: AnthropicSystemBlock[] = [];
|
|
488
497
|
const sanitizedPrompt = systemPrompt ? sanitizeSurrogates(systemPrompt) : "";
|
|
489
498
|
const hasClaudeCodeInstruction = sanitizedPrompt.includes(claudeCodeSystemInstruction);
|
|
490
|
-
const cacheControl = includeCacheControl ? { type: "ephemeral" as const } : undefined;
|
|
491
499
|
|
|
492
500
|
if (includeClaudeCodeInstruction && !hasClaudeCodeInstruction) {
|
|
493
501
|
blocks.push({
|
|
494
502
|
type: "text",
|
|
495
503
|
text: claudeCodeSystemInstruction,
|
|
496
|
-
...(cacheControl ? { cache_control: cacheControl } : {}),
|
|
497
504
|
});
|
|
498
505
|
}
|
|
499
506
|
|
|
@@ -503,7 +510,6 @@ export function buildAnthropicSystemBlocks(
|
|
|
503
510
|
blocks.push({
|
|
504
511
|
type: "text",
|
|
505
512
|
text: trimmed,
|
|
506
|
-
...(cacheControl ? { cache_control: cacheControl } : {}),
|
|
507
513
|
});
|
|
508
514
|
}
|
|
509
515
|
|
|
@@ -511,7 +517,6 @@ export function buildAnthropicSystemBlocks(
|
|
|
511
517
|
blocks.push({
|
|
512
518
|
type: "text",
|
|
513
519
|
text: sanitizedPrompt,
|
|
514
|
-
...(cacheControl ? { cache_control: cacheControl } : {}),
|
|
515
520
|
});
|
|
516
521
|
}
|
|
517
522
|
|
|
@@ -546,11 +551,9 @@ function buildParams(
|
|
|
546
551
|
isOAuthToken: boolean,
|
|
547
552
|
options?: AnthropicOptions,
|
|
548
553
|
): MessageCreateParamsStreaming {
|
|
549
|
-
const hasTools = Boolean(context.tools?.length);
|
|
550
|
-
const cacheControlMode = resolveCacheControlMode(context.messages, hasTools && isOAuthToken);
|
|
551
554
|
const params: MessageCreateParamsStreaming = {
|
|
552
555
|
model: model.id,
|
|
553
|
-
messages: convertMessages(context.messages, model, isOAuthToken
|
|
556
|
+
messages: convertMessages(context.messages, model, isOAuthToken),
|
|
554
557
|
max_tokens: options?.maxTokens || (model.maxTokens / 3) | 0,
|
|
555
558
|
stream: true,
|
|
556
559
|
};
|
|
@@ -558,7 +561,6 @@ function buildParams(
|
|
|
558
561
|
const includeClaudeCodeSystem = !model.id.startsWith("claude-3-5-haiku");
|
|
559
562
|
const systemBlocks = buildAnthropicSystemBlocks(context.systemPrompt, {
|
|
560
563
|
includeClaudeCodeInstruction: includeClaudeCodeSystem,
|
|
561
|
-
includeCacheControl: cacheControlMode !== "none",
|
|
562
564
|
});
|
|
563
565
|
if (systemBlocks) {
|
|
564
566
|
params.system = systemBlocks;
|
|
@@ -596,6 +598,8 @@ function buildParams(
|
|
|
596
598
|
ensureMaxTokensForThinking(params, model);
|
|
597
599
|
}
|
|
598
600
|
|
|
601
|
+
applyPromptCaching(params);
|
|
602
|
+
|
|
599
603
|
return params;
|
|
600
604
|
}
|
|
601
605
|
|
|
@@ -605,75 +609,141 @@ function sanitizeToolCallId(id: string): string {
|
|
|
605
609
|
return id.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
606
610
|
}
|
|
607
611
|
|
|
608
|
-
function
|
|
609
|
-
|
|
612
|
+
function stripCacheControl<T extends CacheControlBlock>(blocks: T[]): void {
|
|
613
|
+
for (const block of blocks) {
|
|
614
|
+
if ("cache_control" in block) {
|
|
615
|
+
delete block.cache_control;
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
function applyCacheControlToLastBlock<T extends CacheControlBlock>(blocks: T[]): void {
|
|
621
|
+
if (blocks.length === 0) return;
|
|
622
|
+
const lastIndex = blocks.length - 1;
|
|
623
|
+
blocks[lastIndex] = { ...blocks[lastIndex], cache_control: cacheControlEphemeral };
|
|
624
|
+
}
|
|
610
625
|
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
626
|
+
function applyCacheControlToLastTextBlock(blocks: Array<ContentBlockParam & CacheControlBlock>): void {
|
|
627
|
+
if (blocks.length === 0) return;
|
|
628
|
+
for (let i = blocks.length - 1; i >= 0; i--) {
|
|
629
|
+
if (blocks[i].type === "text") {
|
|
630
|
+
blocks[i] = { ...blocks[i], cache_control: cacheControlEphemeral };
|
|
631
|
+
return;
|
|
616
632
|
}
|
|
617
633
|
}
|
|
634
|
+
applyCacheControlToLastBlock(blocks);
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
function applyPromptCaching(params: MessageCreateParamsStreaming): void {
|
|
638
|
+
// Anthropic allows max 4 cache breakpoints
|
|
639
|
+
const MAX_CACHE_BREAKPOINTS = 4;
|
|
618
640
|
|
|
619
|
-
|
|
641
|
+
// First, strip ALL existing cache_control to ensure clean slate
|
|
642
|
+
if (params.tools) {
|
|
643
|
+
for (const tool of params.tools) {
|
|
644
|
+
delete (tool as CacheControlBlock).cache_control;
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
if (params.system && Array.isArray(params.system)) {
|
|
649
|
+
stripCacheControl(params.system);
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
for (const message of params.messages) {
|
|
653
|
+
if (Array.isArray(message.content)) {
|
|
654
|
+
stripCacheControl(message.content as Array<ContentBlockParam & CacheControlBlock>);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
let cacheBreakpointsUsed = 0;
|
|
659
|
+
|
|
660
|
+
// Cache hierarchy order: tools -> system -> messages
|
|
661
|
+
// See: https://docs.anthropic.com/en/docs/build-with-claude/prompt-caching
|
|
662
|
+
|
|
663
|
+
// 1. Cache tools - place breakpoint on last tool definition
|
|
664
|
+
if (params.tools && params.tools.length > 0) {
|
|
665
|
+
applyCacheControlToLastBlock(params.tools as Array<CacheControlBlock>);
|
|
666
|
+
cacheBreakpointsUsed++;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
if (cacheBreakpointsUsed >= MAX_CACHE_BREAKPOINTS) return;
|
|
670
|
+
|
|
671
|
+
// 2. Cache system prompt
|
|
672
|
+
if (params.system && Array.isArray(params.system) && params.system.length > 0) {
|
|
673
|
+
applyCacheControlToLastBlock(params.system);
|
|
674
|
+
cacheBreakpointsUsed++;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
if (cacheBreakpointsUsed >= MAX_CACHE_BREAKPOINTS) return;
|
|
678
|
+
|
|
679
|
+
// 3. Cache penultimate user message for conversation history caching
|
|
680
|
+
const userIndexes = params.messages
|
|
681
|
+
.map((message, index) => (message.role === "user" ? index : -1))
|
|
682
|
+
.filter((index) => index >= 0);
|
|
683
|
+
|
|
684
|
+
if (userIndexes.length >= 2) {
|
|
685
|
+
const penultimateUserIndex = userIndexes[userIndexes.length - 2];
|
|
686
|
+
const penultimateUser = params.messages[penultimateUserIndex];
|
|
687
|
+
if (penultimateUser) {
|
|
688
|
+
if (typeof penultimateUser.content === "string") {
|
|
689
|
+
penultimateUser.content = [
|
|
690
|
+
{ type: "text", text: penultimateUser.content, cache_control: cacheControlEphemeral },
|
|
691
|
+
];
|
|
692
|
+
cacheBreakpointsUsed++;
|
|
693
|
+
} else if (Array.isArray(penultimateUser.content) && penultimateUser.content.length > 0) {
|
|
694
|
+
applyCacheControlToLastTextBlock(penultimateUser.content as Array<ContentBlockParam & CacheControlBlock>);
|
|
695
|
+
cacheBreakpointsUsed++;
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
if (cacheBreakpointsUsed >= MAX_CACHE_BREAKPOINTS) return;
|
|
701
|
+
|
|
702
|
+
// 4. Cache final user message for current turn (enables cache hit on next request)
|
|
703
|
+
if (userIndexes.length >= 1) {
|
|
704
|
+
const lastUserIndex = userIndexes[userIndexes.length - 1];
|
|
705
|
+
const lastUser = params.messages[lastUserIndex];
|
|
706
|
+
if (lastUser) {
|
|
707
|
+
if (typeof lastUser.content === "string") {
|
|
708
|
+
lastUser.content = [{ type: "text", text: lastUser.content, cache_control: cacheControlEphemeral }];
|
|
709
|
+
} else if (Array.isArray(lastUser.content) && lastUser.content.length > 0) {
|
|
710
|
+
applyCacheControlToLastTextBlock(lastUser.content as Array<ContentBlockParam & CacheControlBlock>);
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
}
|
|
620
714
|
}
|
|
621
715
|
|
|
622
716
|
function convertMessages(
|
|
623
717
|
messages: Message[],
|
|
624
718
|
model: Model<"anthropic-messages">,
|
|
625
719
|
isOAuthToken: boolean,
|
|
626
|
-
cacheControlMode: CacheControlMode,
|
|
627
720
|
): MessageParam[] {
|
|
628
721
|
const params: MessageParam[] = [];
|
|
629
|
-
const applyToolCacheControl = cacheControlMode === "toolBlocks";
|
|
630
|
-
const applyUserTextCacheControl = cacheControlMode === "userText";
|
|
631
|
-
const withCacheControl = <T extends object>(block: T, enabled: boolean): T | (T & CacheControlBlock) => {
|
|
632
|
-
if (!enabled) return block;
|
|
633
|
-
return { ...block, cache_control: cacheControlEphemeral };
|
|
634
|
-
};
|
|
635
722
|
|
|
636
723
|
// Transform messages for cross-provider compatibility
|
|
637
724
|
const transformedMessages = transformMessages(messages, model);
|
|
638
|
-
|
|
639
725
|
for (let i = 0; i < transformedMessages.length; i++) {
|
|
640
726
|
const msg = transformedMessages[i];
|
|
641
727
|
|
|
642
728
|
if (msg.role === "user") {
|
|
729
|
+
// Skip messages with undefined/null content
|
|
730
|
+
if (!msg.content) continue;
|
|
731
|
+
|
|
643
732
|
if (typeof msg.content === "string") {
|
|
644
733
|
if (msg.content.trim().length > 0) {
|
|
645
734
|
const text = sanitizeSurrogates(msg.content);
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
type: "text",
|
|
651
|
-
text,
|
|
652
|
-
},
|
|
653
|
-
true,
|
|
654
|
-
),
|
|
655
|
-
];
|
|
656
|
-
params.push({
|
|
657
|
-
role: "user",
|
|
658
|
-
content: blocks,
|
|
659
|
-
});
|
|
660
|
-
} else {
|
|
661
|
-
params.push({
|
|
662
|
-
role: "user",
|
|
663
|
-
content: text,
|
|
664
|
-
});
|
|
665
|
-
}
|
|
735
|
+
params.push({
|
|
736
|
+
role: "user",
|
|
737
|
+
content: text,
|
|
738
|
+
});
|
|
666
739
|
}
|
|
667
|
-
} else {
|
|
740
|
+
} else if (Array.isArray(msg.content)) {
|
|
668
741
|
const blocks: Array<ContentBlockParam & CacheControlBlock> = msg.content.map((item) => {
|
|
669
742
|
if (item.type === "text") {
|
|
670
|
-
return
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
},
|
|
675
|
-
applyUserTextCacheControl,
|
|
676
|
-
);
|
|
743
|
+
return {
|
|
744
|
+
type: "text",
|
|
745
|
+
text: sanitizeSurrogates(item.text),
|
|
746
|
+
};
|
|
677
747
|
}
|
|
678
748
|
return {
|
|
679
749
|
type: "image",
|
|
@@ -698,6 +768,9 @@ function convertMessages(
|
|
|
698
768
|
});
|
|
699
769
|
}
|
|
700
770
|
} else if (msg.role === "assistant") {
|
|
771
|
+
// Skip messages with undefined/null content
|
|
772
|
+
if (!msg.content || !Array.isArray(msg.content)) continue;
|
|
773
|
+
|
|
701
774
|
const blocks: Array<ContentBlockParam & CacheControlBlock> = [];
|
|
702
775
|
|
|
703
776
|
for (const block of msg.content) {
|
|
@@ -725,17 +798,12 @@ function convertMessages(
|
|
|
725
798
|
});
|
|
726
799
|
}
|
|
727
800
|
} else if (block.type === "toolCall") {
|
|
728
|
-
blocks.push(
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
input: block.arguments,
|
|
735
|
-
},
|
|
736
|
-
applyToolCacheControl,
|
|
737
|
-
),
|
|
738
|
-
);
|
|
801
|
+
blocks.push({
|
|
802
|
+
type: "tool_use",
|
|
803
|
+
id: sanitizeToolCallId(block.id),
|
|
804
|
+
name: isOAuthToken ? toClaudeCodeName(block.name) : block.name,
|
|
805
|
+
input: block.arguments,
|
|
806
|
+
});
|
|
739
807
|
}
|
|
740
808
|
}
|
|
741
809
|
if (blocks.length === 0) continue;
|
|
@@ -748,33 +816,23 @@ function convertMessages(
|
|
|
748
816
|
const toolResults: Array<ContentBlockParam & CacheControlBlock> = [];
|
|
749
817
|
|
|
750
818
|
// Add the current tool result
|
|
751
|
-
toolResults.push(
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
is_error: msg.isError,
|
|
758
|
-
},
|
|
759
|
-
applyToolCacheControl,
|
|
760
|
-
),
|
|
761
|
-
);
|
|
819
|
+
toolResults.push({
|
|
820
|
+
type: "tool_result",
|
|
821
|
+
tool_use_id: sanitizeToolCallId(msg.toolCallId),
|
|
822
|
+
content: convertContentBlocks(msg.content),
|
|
823
|
+
is_error: msg.isError,
|
|
824
|
+
});
|
|
762
825
|
|
|
763
826
|
// Look ahead for consecutive toolResult messages
|
|
764
827
|
let j = i + 1;
|
|
765
828
|
while (j < transformedMessages.length && transformedMessages[j].role === "toolResult") {
|
|
766
829
|
const nextMsg = transformedMessages[j] as ToolResultMessage; // We know it's a toolResult
|
|
767
|
-
toolResults.push(
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
is_error: nextMsg.isError,
|
|
774
|
-
},
|
|
775
|
-
applyToolCacheControl,
|
|
776
|
-
),
|
|
777
|
-
);
|
|
830
|
+
toolResults.push({
|
|
831
|
+
type: "tool_result",
|
|
832
|
+
tool_use_id: sanitizeToolCallId(nextMsg.toolCallId),
|
|
833
|
+
content: convertContentBlocks(nextMsg.content),
|
|
834
|
+
is_error: nextMsg.isError,
|
|
835
|
+
});
|
|
778
836
|
j++;
|
|
779
837
|
}
|
|
780
838
|
|
|
@@ -782,14 +840,22 @@ function convertMessages(
|
|
|
782
840
|
i = j - 1;
|
|
783
841
|
|
|
784
842
|
// Add a single user message with all tool results
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
843
|
+
if (toolResults.length > 0) {
|
|
844
|
+
params.push({
|
|
845
|
+
role: "user",
|
|
846
|
+
content: toolResults,
|
|
847
|
+
});
|
|
848
|
+
}
|
|
789
849
|
}
|
|
790
850
|
}
|
|
791
851
|
|
|
792
|
-
|
|
852
|
+
// Final validation: filter out any messages with invalid content
|
|
853
|
+
return params.filter((msg) => {
|
|
854
|
+
if (!msg.content) return false;
|
|
855
|
+
if (typeof msg.content === "string") return msg.content.length > 0;
|
|
856
|
+
if (Array.isArray(msg.content)) return msg.content.length > 0;
|
|
857
|
+
return false;
|
|
858
|
+
});
|
|
793
859
|
}
|
|
794
860
|
|
|
795
861
|
function convertTools(tools: Tool[], isOAuthToken: boolean): Anthropic.Messages.Tool[] {
|
package/src/providers/cursor.ts
CHANGED
|
@@ -410,6 +410,7 @@ export const streamGoogleGeminiCli: StreamFunction<"google-gemini-cli"> = (
|
|
|
410
410
|
const endpoints = baseUrl ? [baseUrl] : isAntigravity ? ANTIGRAVITY_ENDPOINT_FALLBACKS : [DEFAULT_ENDPOINT];
|
|
411
411
|
|
|
412
412
|
const requestBody = buildRequest(model, context, projectId, options, isAntigravity);
|
|
413
|
+
options?.onPayload?.(requestBody);
|
|
413
414
|
const headers = isAntigravity ? ANTIGRAVITY_HEADERS : GEMINI_CLI_HEADERS;
|
|
414
415
|
|
|
415
416
|
const requestHeaders = {
|
|
@@ -418,6 +419,7 @@ export const streamGoogleGeminiCli: StreamFunction<"google-gemini-cli"> = (
|
|
|
418
419
|
Accept: "text/event-stream",
|
|
419
420
|
...headers,
|
|
420
421
|
...(isClaudeThinkingModel(model.id) ? { "anthropic-beta": CLAUDE_THINKING_BETA_HEADER } : {}),
|
|
422
|
+
...(options?.headers ?? {}),
|
|
421
423
|
};
|
|
422
424
|
const requestBodyJson = JSON.stringify(requestBody);
|
|
423
425
|
|
|
@@ -85,6 +85,7 @@ export const streamGoogleVertex: StreamFunction<"google-vertex"> = (
|
|
|
85
85
|
const location = resolveLocation(options);
|
|
86
86
|
const client = createClient(model, project, location);
|
|
87
87
|
const params = buildParams(model, context, options);
|
|
88
|
+
options?.onPayload?.(params);
|
|
88
89
|
const googleStream = await client.models.generateContentStream(params);
|
|
89
90
|
|
|
90
91
|
stream.push({ type: "start", partial: output });
|
package/src/providers/google.ts
CHANGED
|
@@ -75,6 +75,7 @@ export const streamGoogle: StreamFunction<"google-generative-ai"> = (
|
|
|
75
75
|
const apiKey = options?.apiKey || getEnvApiKey(model.provider) || "";
|
|
76
76
|
const client = createClient(model, apiKey);
|
|
77
77
|
const params = buildParams(model, context, options);
|
|
78
|
+
options?.onPayload?.(params);
|
|
78
79
|
const googleStream = await client.models.generateContentStream(params);
|
|
79
80
|
|
|
80
81
|
stream.push({ type: "start", partial: output });
|
|
@@ -55,6 +55,29 @@ const CODEX_MAX_RETRIES = 2;
|
|
|
55
55
|
const CODEX_RETRYABLE_STATUS = new Set([408, 429, 500, 502, 503, 504]);
|
|
56
56
|
const CODEX_RETRY_DELAY_MS = 500;
|
|
57
57
|
|
|
58
|
+
/** Fast deterministic hash to shorten long strings */
|
|
59
|
+
function shortHash(str: string): string {
|
|
60
|
+
let h1 = 0xdeadbeef;
|
|
61
|
+
let h2 = 0x41c6ce57;
|
|
62
|
+
for (let i = 0; i < str.length; i++) {
|
|
63
|
+
const ch = str.charCodeAt(i);
|
|
64
|
+
h1 = Math.imul(h1 ^ ch, 2654435761);
|
|
65
|
+
h2 = Math.imul(h2 ^ ch, 1597334677);
|
|
66
|
+
}
|
|
67
|
+
h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^ Math.imul(h2 ^ (h2 >>> 13), 3266489909);
|
|
68
|
+
h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^ Math.imul(h1 ^ (h1 >>> 13), 3266489909);
|
|
69
|
+
return (h2 >>> 0).toString(36) + (h1 >>> 0).toString(36);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function normalizeResponsesToolCallId(id: string): { callId: string; itemId: string } {
|
|
73
|
+
const [callId, itemId] = id.split("|");
|
|
74
|
+
if (callId && itemId) {
|
|
75
|
+
return { callId, itemId };
|
|
76
|
+
}
|
|
77
|
+
const hash = shortHash(id);
|
|
78
|
+
return { callId: `call_${hash}`, itemId: `item_${hash}` };
|
|
79
|
+
}
|
|
80
|
+
|
|
58
81
|
export const streamOpenAICodexResponses: StreamFunction<"openai-codex-responses"> = (
|
|
59
82
|
model: Model<"openai-codex-responses">,
|
|
60
83
|
context: Context,
|
|
@@ -128,9 +151,15 @@ export const streamOpenAICodexResponses: StreamFunction<"openai-codex-responses"
|
|
|
128
151
|
};
|
|
129
152
|
|
|
130
153
|
const transformedBody = await transformRequestBody(params, codexOptions, systemPrompt);
|
|
154
|
+
options?.onPayload?.(transformedBody);
|
|
131
155
|
|
|
132
156
|
const reasoningEffort = transformedBody.reasoning?.effort ?? null;
|
|
133
|
-
const headers = createCodexHeaders(
|
|
157
|
+
const headers = createCodexHeaders(
|
|
158
|
+
{ ...(model.headers ?? {}), ...(options?.headers ?? {}) },
|
|
159
|
+
accountId,
|
|
160
|
+
apiKey,
|
|
161
|
+
options?.sessionId,
|
|
162
|
+
);
|
|
134
163
|
logCodexDebug("codex request", {
|
|
135
164
|
url,
|
|
136
165
|
model: params.model,
|
|
@@ -508,19 +537,6 @@ function getAccountId(accessToken: string): string {
|
|
|
508
537
|
return accountId;
|
|
509
538
|
}
|
|
510
539
|
|
|
511
|
-
function shortHash(str: string): string {
|
|
512
|
-
let h1 = 0xdeadbeef;
|
|
513
|
-
let h2 = 0x41c6ce57;
|
|
514
|
-
for (let i = 0; i < str.length; i++) {
|
|
515
|
-
const ch = str.charCodeAt(i);
|
|
516
|
-
h1 = Math.imul(h1 ^ ch, 2654435761);
|
|
517
|
-
h2 = Math.imul(h2 ^ ch, 1597334677);
|
|
518
|
-
}
|
|
519
|
-
h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^ Math.imul(h2 ^ (h2 >>> 13), 3266489909);
|
|
520
|
-
h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^ Math.imul(h1 ^ (h1 >>> 13), 3266489909);
|
|
521
|
-
return (h2 >>> 0).toString(36) + (h1 >>> 0).toString(36);
|
|
522
|
-
}
|
|
523
|
-
|
|
524
540
|
function convertMessages(model: Model<"openai-codex-responses">, context: Context): ResponseInput {
|
|
525
541
|
const messages: ResponseInput = [];
|
|
526
542
|
|
|
@@ -583,10 +599,11 @@ function convertMessages(model: Model<"openai-codex-responses">, context: Contex
|
|
|
583
599
|
} satisfies ResponseOutputMessage);
|
|
584
600
|
} else if (block.type === "toolCall" && msg.stopReason !== "error") {
|
|
585
601
|
const toolCall = block as ToolCall;
|
|
602
|
+
const normalized = normalizeResponsesToolCallId(toolCall.id);
|
|
586
603
|
output.push({
|
|
587
604
|
type: "function_call",
|
|
588
|
-
id:
|
|
589
|
-
call_id:
|
|
605
|
+
id: normalized.itemId,
|
|
606
|
+
call_id: normalized.callId,
|
|
590
607
|
name: toolCall.name,
|
|
591
608
|
arguments: JSON.stringify(toolCall.arguments),
|
|
592
609
|
});
|
|
@@ -600,11 +617,12 @@ function convertMessages(model: Model<"openai-codex-responses">, context: Contex
|
|
|
600
617
|
.map((c) => (c as { text: string }).text)
|
|
601
618
|
.join("\n");
|
|
602
619
|
const hasImages = msg.content.some((c) => c.type === "image");
|
|
620
|
+
const normalized = normalizeResponsesToolCallId(msg.toolCallId);
|
|
603
621
|
|
|
604
622
|
const hasText = textResult.length > 0;
|
|
605
623
|
messages.push({
|
|
606
624
|
type: "function_call_output",
|
|
607
|
-
call_id:
|
|
625
|
+
call_id: normalized.callId,
|
|
608
626
|
output: sanitizeSurrogates(hasText ? textResult : "(see attached image)"),
|
|
609
627
|
});
|
|
610
628
|
|
|
@@ -101,8 +101,9 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions"> = (
|
|
|
101
101
|
|
|
102
102
|
try {
|
|
103
103
|
const apiKey = options?.apiKey || getEnvApiKey(model.provider) || "";
|
|
104
|
-
const client = createClient(model, context, apiKey);
|
|
104
|
+
const client = createClient(model, context, apiKey, options?.headers);
|
|
105
105
|
const params = buildParams(model, context, options);
|
|
106
|
+
options?.onPayload?.(params);
|
|
106
107
|
const openaiStream = await client.chat.completions.create(params, { signal: options?.signal });
|
|
107
108
|
stream.push({ type: "start", partial: output });
|
|
108
109
|
|
|
@@ -319,7 +320,12 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions"> = (
|
|
|
319
320
|
return stream;
|
|
320
321
|
};
|
|
321
322
|
|
|
322
|
-
function createClient(
|
|
323
|
+
function createClient(
|
|
324
|
+
model: Model<"openai-completions">,
|
|
325
|
+
context: Context,
|
|
326
|
+
apiKey?: string,
|
|
327
|
+
extraHeaders?: Record<string, string>,
|
|
328
|
+
) {
|
|
323
329
|
if (!apiKey) {
|
|
324
330
|
if (!process.env.OPENAI_API_KEY) {
|
|
325
331
|
throw new Error(
|
|
@@ -329,7 +335,7 @@ function createClient(model: Model<"openai-completions">, context: Context, apiK
|
|
|
329
335
|
apiKey = process.env.OPENAI_API_KEY;
|
|
330
336
|
}
|
|
331
337
|
|
|
332
|
-
const headers = { ...model.headers };
|
|
338
|
+
const headers = { ...(model.headers ?? {}), ...(extraHeaders ?? {}) };
|
|
333
339
|
if (model.provider === "github-copilot") {
|
|
334
340
|
// Copilot expects X-Initiator to indicate whether the request is user-initiated
|
|
335
341
|
// or agent-initiated (e.g. follow-up after assistant/tool messages). If there is
|
|
@@ -50,6 +50,11 @@ export interface OpenAIResponsesOptions extends StreamOptions {
|
|
|
50
50
|
reasoningEffort?: "minimal" | "low" | "medium" | "high" | "xhigh";
|
|
51
51
|
reasoningSummary?: "auto" | "detailed" | "concise" | null;
|
|
52
52
|
serviceTier?: ResponseCreateParamsStreaming["service_tier"];
|
|
53
|
+
/**
|
|
54
|
+
* Enforce strict tool call/result pairing when building Responses API inputs.
|
|
55
|
+
* Azure OpenAI Responses API requires tool results to have a matching tool call.
|
|
56
|
+
*/
|
|
57
|
+
strictResponsesPairing?: boolean;
|
|
53
58
|
}
|
|
54
59
|
|
|
55
60
|
/**
|
|
@@ -85,8 +90,9 @@ export const streamOpenAIResponses: StreamFunction<"openai-responses"> = (
|
|
|
85
90
|
try {
|
|
86
91
|
// Create OpenAI client
|
|
87
92
|
const apiKey = options?.apiKey || getEnvApiKey(model.provider) || "";
|
|
88
|
-
const client = createClient(model, context, apiKey);
|
|
93
|
+
const client = createClient(model, context, apiKey, options?.headers);
|
|
89
94
|
const params = buildParams(model, context, options);
|
|
95
|
+
options?.onPayload?.(params);
|
|
90
96
|
const openaiStream = await client.responses.create(
|
|
91
97
|
params,
|
|
92
98
|
options?.signal ? { signal: options.signal } : undefined,
|
|
@@ -317,7 +323,12 @@ export const streamOpenAIResponses: StreamFunction<"openai-responses"> = (
|
|
|
317
323
|
return stream;
|
|
318
324
|
};
|
|
319
325
|
|
|
320
|
-
function createClient(
|
|
326
|
+
function createClient(
|
|
327
|
+
model: Model<"openai-responses">,
|
|
328
|
+
context: Context,
|
|
329
|
+
apiKey?: string,
|
|
330
|
+
extraHeaders?: Record<string, string>,
|
|
331
|
+
) {
|
|
321
332
|
if (!apiKey) {
|
|
322
333
|
if (!process.env.OPENAI_API_KEY) {
|
|
323
334
|
throw new Error(
|
|
@@ -327,7 +338,7 @@ function createClient(model: Model<"openai-responses">, context: Context, apiKey
|
|
|
327
338
|
apiKey = process.env.OPENAI_API_KEY;
|
|
328
339
|
}
|
|
329
340
|
|
|
330
|
-
const headers = { ...model.headers };
|
|
341
|
+
const headers = { ...(model.headers ?? {}), ...(extraHeaders ?? {}) };
|
|
331
342
|
if (model.provider === "github-copilot") {
|
|
332
343
|
// Copilot expects X-Initiator to indicate whether the request is user-initiated
|
|
333
344
|
// or agent-initiated (e.g. follow-up after assistant/tool messages). If there is
|
|
@@ -362,7 +373,8 @@ function createClient(model: Model<"openai-responses">, context: Context, apiKey
|
|
|
362
373
|
}
|
|
363
374
|
|
|
364
375
|
function buildParams(model: Model<"openai-responses">, context: Context, options?: OpenAIResponsesOptions) {
|
|
365
|
-
const
|
|
376
|
+
const strictResponsesPairing = options?.strictResponsesPairing ?? isAzureOpenAIBaseUrl(model.baseUrl ?? "");
|
|
377
|
+
const messages = convertMessages(model, context, strictResponsesPairing);
|
|
366
378
|
|
|
367
379
|
const params: ResponseCreateParamsStreaming = {
|
|
368
380
|
model: model.id,
|
|
@@ -413,8 +425,26 @@ function buildParams(model: Model<"openai-responses">, context: Context, options
|
|
|
413
425
|
return params;
|
|
414
426
|
}
|
|
415
427
|
|
|
416
|
-
function
|
|
428
|
+
function normalizeResponsesToolCallId(id: string): { callId: string; itemId: string } {
|
|
429
|
+
const [callId, itemId] = id.split("|");
|
|
430
|
+
if (callId && itemId) {
|
|
431
|
+
return { callId, itemId };
|
|
432
|
+
}
|
|
433
|
+
const hash = shortHash(id);
|
|
434
|
+
return { callId: `call_${hash}`, itemId: `item_${hash}` };
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function isAzureOpenAIBaseUrl(baseUrl: string): boolean {
|
|
438
|
+
return baseUrl.includes(".openai.azure.com") || baseUrl.includes("azure.com/openai");
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function convertMessages(
|
|
442
|
+
model: Model<"openai-responses">,
|
|
443
|
+
context: Context,
|
|
444
|
+
strictResponsesPairing: boolean,
|
|
445
|
+
): ResponseInput {
|
|
417
446
|
const messages: ResponseInput = [];
|
|
447
|
+
const knownCallIds = new Set<string>();
|
|
418
448
|
|
|
419
449
|
const transformedMessages = transformMessages(context.messages, model);
|
|
420
450
|
|
|
@@ -487,10 +517,12 @@ function convertMessages(model: Model<"openai-responses">, context: Context): Re
|
|
|
487
517
|
// Do not submit toolcall blocks if the completion had an error (i.e. abort)
|
|
488
518
|
} else if (block.type === "toolCall" && msg.stopReason !== "error") {
|
|
489
519
|
const toolCall = block as ToolCall;
|
|
520
|
+
const normalized = normalizeResponsesToolCallId(toolCall.id);
|
|
521
|
+
knownCallIds.add(normalized.callId);
|
|
490
522
|
output.push({
|
|
491
523
|
type: "function_call",
|
|
492
|
-
id:
|
|
493
|
-
call_id:
|
|
524
|
+
id: normalized.itemId,
|
|
525
|
+
call_id: normalized.callId,
|
|
494
526
|
name: toolCall.name,
|
|
495
527
|
arguments: JSON.stringify(toolCall.arguments),
|
|
496
528
|
});
|
|
@@ -505,12 +537,16 @@ function convertMessages(model: Model<"openai-responses">, context: Context): Re
|
|
|
505
537
|
.map((c) => (c as any).text)
|
|
506
538
|
.join("\n");
|
|
507
539
|
const hasImages = msg.content.some((c) => c.type === "image");
|
|
540
|
+
const normalized = normalizeResponsesToolCallId(msg.toolCallId);
|
|
541
|
+
if (strictResponsesPairing && !knownCallIds.has(normalized.callId)) {
|
|
542
|
+
continue;
|
|
543
|
+
}
|
|
508
544
|
|
|
509
545
|
// Always send function_call_output with text (or placeholder if only images)
|
|
510
546
|
const hasText = textResult.length > 0;
|
|
511
547
|
messages.push({
|
|
512
548
|
type: "function_call_output",
|
|
513
|
-
call_id:
|
|
549
|
+
call_id: normalized.callId,
|
|
514
550
|
output: sanitizeSurrogates(hasText ? textResult : "(see attached image)"),
|
|
515
551
|
});
|
|
516
552
|
|
|
@@ -9,9 +9,34 @@ function normalizeToolCallId(id: string): string {
|
|
|
9
9
|
return id.replace(/[^a-zA-Z0-9_-]/g, "").slice(0, 40);
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
/** Fast deterministic hash to shorten long strings */
|
|
13
|
+
function shortHash(str: string): string {
|
|
14
|
+
let h1 = 0xdeadbeef;
|
|
15
|
+
let h2 = 0x41c6ce57;
|
|
16
|
+
for (let i = 0; i < str.length; i++) {
|
|
17
|
+
const ch = str.charCodeAt(i);
|
|
18
|
+
h1 = Math.imul(h1 ^ ch, 2654435761);
|
|
19
|
+
h2 = Math.imul(h2 ^ ch, 1597334677);
|
|
20
|
+
}
|
|
21
|
+
h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^ Math.imul(h2 ^ (h2 >>> 13), 3266489909);
|
|
22
|
+
h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^ Math.imul(h1 ^ (h1 >>> 13), 3266489909);
|
|
23
|
+
return (h2 >>> 0).toString(36) + (h1 >>> 0).toString(36);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function normalizeResponsesToolCallId(id: string): string {
|
|
27
|
+
const [callId, itemId] = id.split("|");
|
|
28
|
+
if (callId && itemId) {
|
|
29
|
+
return id;
|
|
30
|
+
}
|
|
31
|
+
const hash = shortHash(id);
|
|
32
|
+
return `call_${hash}|item_${hash}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
12
35
|
export function transformMessages<TApi extends Api>(messages: Message[], model: Model<TApi>): Message[] {
|
|
13
36
|
// Build a map of original tool call IDs to normalized IDs for github-copilot cross-API switches
|
|
14
37
|
const toolCallIdMap = new Map<string, string>();
|
|
38
|
+
const skippedToolCallIds = new Set<string>();
|
|
39
|
+
const needsResponsesToolCallIds = model.api === "openai-responses" || model.api === "openai-codex-responses";
|
|
15
40
|
|
|
16
41
|
// First pass: transform messages (thinking blocks, tool call ID normalization)
|
|
17
42
|
const transformed = messages.flatMap<Message>((msg): Message[] => {
|
|
@@ -22,20 +47,39 @@ export function transformMessages<TApi extends Api>(messages: Message[], model:
|
|
|
22
47
|
|
|
23
48
|
// Handle toolResult messages - normalize toolCallId if we have a mapping
|
|
24
49
|
if (msg.role === "toolResult") {
|
|
50
|
+
if (skippedToolCallIds.has(msg.toolCallId)) {
|
|
51
|
+
return [];
|
|
52
|
+
}
|
|
25
53
|
const normalizedId = toolCallIdMap.get(msg.toolCallId);
|
|
26
54
|
if (normalizedId && normalizedId !== msg.toolCallId) {
|
|
27
55
|
return [{ ...msg, toolCallId: normalizedId }];
|
|
28
56
|
}
|
|
57
|
+
if (needsResponsesToolCallIds) {
|
|
58
|
+
return [{ ...msg, toolCallId: normalizeResponsesToolCallId(msg.toolCallId) }];
|
|
59
|
+
}
|
|
29
60
|
return [msg];
|
|
30
61
|
}
|
|
31
62
|
|
|
32
63
|
// Assistant messages need transformation check
|
|
33
64
|
if (msg.role === "assistant") {
|
|
34
65
|
const assistantMsg = msg as AssistantMessage;
|
|
66
|
+
const isSameProviderApi = assistantMsg.provider === model.provider && assistantMsg.api === model.api;
|
|
67
|
+
const isErroredAssistant = assistantMsg.stopReason === "error" || assistantMsg.stopReason === "aborted";
|
|
68
|
+
if (!isSameProviderApi && isErroredAssistant) {
|
|
69
|
+
for (const block of assistantMsg.content) {
|
|
70
|
+
if (block.type === "toolCall") {
|
|
71
|
+
skippedToolCallIds.add(block.id);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return [];
|
|
75
|
+
}
|
|
35
76
|
|
|
36
77
|
// If message is from the same provider and API, keep as is
|
|
37
|
-
if (
|
|
38
|
-
if (
|
|
78
|
+
if (isSameProviderApi) {
|
|
79
|
+
if (
|
|
80
|
+
(assistantMsg.stopReason === "error" || assistantMsg.stopReason === "aborted") &&
|
|
81
|
+
assistantMsg.content.length === 0
|
|
82
|
+
) {
|
|
39
83
|
return [];
|
|
40
84
|
}
|
|
41
85
|
return [msg];
|
|
@@ -64,12 +108,20 @@ export function transformMessages<TApi extends Api>(messages: Message[], model:
|
|
|
64
108
|
};
|
|
65
109
|
}
|
|
66
110
|
// Normalize tool call IDs when target API requires strict format
|
|
67
|
-
if (block.type === "toolCall"
|
|
111
|
+
if (block.type === "toolCall") {
|
|
68
112
|
const toolCall = block as ToolCall;
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
113
|
+
if (needsResponsesToolCallIds) {
|
|
114
|
+
const normalizedId = normalizeResponsesToolCallId(toolCall.id);
|
|
115
|
+
if (normalizedId !== toolCall.id) {
|
|
116
|
+
toolCallIdMap.set(toolCall.id, normalizedId);
|
|
117
|
+
return { ...toolCall, id: normalizedId };
|
|
118
|
+
}
|
|
119
|
+
} else if (needsToolCallIdNormalization) {
|
|
120
|
+
const normalizedId = normalizeToolCallId(toolCall.id);
|
|
121
|
+
if (normalizedId !== toolCall.id) {
|
|
122
|
+
toolCallIdMap.set(toolCall.id, normalizedId);
|
|
123
|
+
return { ...toolCall, id: normalizedId };
|
|
124
|
+
}
|
|
73
125
|
}
|
|
74
126
|
}
|
|
75
127
|
// All other blocks pass through unchanged
|
package/src/stream.ts
CHANGED
|
@@ -79,10 +79,17 @@ export function getEnvApiKey(provider: any): string | undefined {
|
|
|
79
79
|
// 1. AWS_PROFILE - named profile from ~/.aws/credentials
|
|
80
80
|
// 2. AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY - standard IAM keys
|
|
81
81
|
// 3. AWS_BEARER_TOKEN_BEDROCK - Bedrock API keys (bearer token)
|
|
82
|
+
// 4. AWS_CONTAINER_CREDENTIALS_* - ECS/Task IAM role credentials
|
|
83
|
+
// 5. AWS_WEB_IDENTITY_TOKEN_FILE + AWS_ROLE_ARN - IRSA (EKS) web identity
|
|
84
|
+
const hasEcsCredentials =
|
|
85
|
+
!!process.env.AWS_CONTAINER_CREDENTIALS_RELATIVE_URI || !!process.env.AWS_CONTAINER_CREDENTIALS_FULL_URI;
|
|
86
|
+
const hasWebIdentity = !!process.env.AWS_WEB_IDENTITY_TOKEN_FILE && !!process.env.AWS_ROLE_ARN;
|
|
82
87
|
if (
|
|
83
88
|
process.env.AWS_PROFILE ||
|
|
84
89
|
(process.env.AWS_ACCESS_KEY_ID && process.env.AWS_SECRET_ACCESS_KEY) ||
|
|
85
|
-
process.env.AWS_BEARER_TOKEN_BEDROCK
|
|
90
|
+
process.env.AWS_BEARER_TOKEN_BEDROCK ||
|
|
91
|
+
hasEcsCredentials ||
|
|
92
|
+
hasWebIdentity
|
|
86
93
|
) {
|
|
87
94
|
return "<authenticated>";
|
|
88
95
|
}
|
|
@@ -208,11 +215,11 @@ export const OUTPUT_FALLBACK_BUFFER = 4000;
|
|
|
208
215
|
const ANTHROPIC_USE_INTERLEAVED_THINKING = true;
|
|
209
216
|
|
|
210
217
|
const ANTHROPIC_THINKING: Record<ThinkingLevel, number> = {
|
|
211
|
-
minimal:
|
|
212
|
-
low:
|
|
213
|
-
medium:
|
|
214
|
-
high:
|
|
215
|
-
xhigh:
|
|
218
|
+
minimal: 1024,
|
|
219
|
+
low: 4096,
|
|
220
|
+
medium: 8192,
|
|
221
|
+
high: 16384,
|
|
222
|
+
xhigh: 32768,
|
|
216
223
|
};
|
|
217
224
|
|
|
218
225
|
const GOOGLE_THINKING: Record<ThinkingLevel, number> = {
|
|
@@ -252,7 +259,9 @@ function mapOptionsForApi<TApi extends Api>(
|
|
|
252
259
|
maxTokens: options?.maxTokens || Math.min(model.maxTokens, 32000),
|
|
253
260
|
signal: options?.signal,
|
|
254
261
|
apiKey: apiKey || options?.apiKey,
|
|
262
|
+
headers: options?.headers,
|
|
255
263
|
sessionId: options?.sessionId,
|
|
264
|
+
onPayload: options?.onPayload,
|
|
256
265
|
execHandlers: options?.execHandlers,
|
|
257
266
|
};
|
|
258
267
|
|
package/src/types.ts
CHANGED
|
@@ -96,12 +96,22 @@ export interface StreamOptions {
|
|
|
96
96
|
maxTokens?: number;
|
|
97
97
|
signal?: AbortSignal;
|
|
98
98
|
apiKey?: string;
|
|
99
|
+
/**
|
|
100
|
+
* Additional headers to include in provider requests.
|
|
101
|
+
* These are merged on top of model-defined headers.
|
|
102
|
+
*/
|
|
103
|
+
headers?: Record<string, string>;
|
|
99
104
|
/**
|
|
100
105
|
* Optional session identifier for providers that support session-based caching.
|
|
101
106
|
* Providers can use this to enable prompt caching, request routing, or other
|
|
102
107
|
* session-aware features. Ignored by providers that don't support it.
|
|
103
108
|
*/
|
|
104
109
|
sessionId?: string;
|
|
110
|
+
/**
|
|
111
|
+
* Optional hook to observe the provider request payload before it is sent.
|
|
112
|
+
* The payload format is provider-specific.
|
|
113
|
+
*/
|
|
114
|
+
onPayload?: (payload: unknown) => void;
|
|
105
115
|
/** Cursor exec/MCP tool handlers (cursor-agent only). */
|
|
106
116
|
execHandlers?: CursorExecHandlers;
|
|
107
117
|
}
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
import templateHtml from "./oauth.html" with { type: "text" };
|
|
15
15
|
import type { OAuthController, OAuthCredentials } from "./types";
|
|
16
16
|
|
|
17
|
-
const DEFAULT_TIMEOUT =
|
|
17
|
+
const DEFAULT_TIMEOUT = 120_000;
|
|
18
18
|
const DEFAULT_HOSTNAME = "localhost";
|
|
19
19
|
const CALLBACK_PATH = "/callback";
|
|
20
20
|
|
|
@@ -182,7 +182,7 @@ export abstract class OAuthCallbackFlow {
|
|
|
182
182
|
* Wait for OAuth callback or manual input (whichever comes first).
|
|
183
183
|
*/
|
|
184
184
|
private waitForCallback(expectedState: string): Promise<CallbackResult> {
|
|
185
|
-
const timeoutSignal = AbortSignal.timeout(DEFAULT_TIMEOUT
|
|
185
|
+
const timeoutSignal = AbortSignal.timeout(DEFAULT_TIMEOUT);
|
|
186
186
|
const signal = this.ctrl.signal ? AbortSignal.any([this.ctrl.signal, timeoutSignal]) : timeoutSignal;
|
|
187
187
|
|
|
188
188
|
const callbackPromise = new Promise<CallbackResult>((resolve, reject) => {
|
package/src/utils/oauth/index.ts
CHANGED
|
@@ -30,6 +30,7 @@ export {
|
|
|
30
30
|
export { loginAntigravity, refreshAntigravityToken } from "./google-antigravity";
|
|
31
31
|
// Google Gemini CLI
|
|
32
32
|
export { loginGeminiCli, refreshGoogleCloudToken } from "./google-gemini-cli";
|
|
33
|
+
export type { OpenAICodexLoginOptions } from "./openai-codex";
|
|
33
34
|
// OpenAI Codex (ChatGPT OAuth)
|
|
34
35
|
export { loginOpenAICodex, refreshOpenAICodexToken } from "./openai-codex";
|
|
35
36
|
|
|
@@ -49,6 +49,7 @@ class OpenAICodexOAuthFlow extends OAuthCallbackFlow {
|
|
|
49
49
|
constructor(
|
|
50
50
|
ctrl: OAuthController,
|
|
51
51
|
private readonly pkce: PKCE,
|
|
52
|
+
private readonly originator: string,
|
|
52
53
|
) {
|
|
53
54
|
super(ctrl, CALLBACK_PORT, CALLBACK_PATH);
|
|
54
55
|
}
|
|
@@ -67,7 +68,7 @@ class OpenAICodexOAuthFlow extends OAuthCallbackFlow {
|
|
|
67
68
|
state,
|
|
68
69
|
id_token_add_organizations: "true",
|
|
69
70
|
codex_cli_simplified_flow: "true",
|
|
70
|
-
originator:
|
|
71
|
+
originator: this.originator,
|
|
71
72
|
});
|
|
72
73
|
|
|
73
74
|
const url = `${AUTHORIZE_URL}?${searchParams.toString()}`;
|
|
@@ -122,9 +123,15 @@ async function exchangeCodeForToken(code: string, verifier: string, redirectUri:
|
|
|
122
123
|
/**
|
|
123
124
|
* Login with OpenAI Codex OAuth
|
|
124
125
|
*/
|
|
125
|
-
export
|
|
126
|
+
export type OpenAICodexLoginOptions = OAuthController & {
|
|
127
|
+
/** Optional originator value for OpenAI Codex OAuth. Default: "opencode". */
|
|
128
|
+
originator?: string;
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
export async function loginOpenAICodex(options: OpenAICodexLoginOptions): Promise<OAuthCredentials> {
|
|
126
132
|
const pkce = await generatePKCE();
|
|
127
|
-
const
|
|
133
|
+
const originator = options.originator?.trim() || "opencode";
|
|
134
|
+
const flow = new OpenAICodexOAuthFlow(options, pkce, originator);
|
|
128
135
|
|
|
129
136
|
return flow.login();
|
|
130
137
|
}
|