@oh-my-pi/pi-ai 6.8.2 → 6.8.3
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 +4 -2
- 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 +10 -1
- package/src/types.ts +10 -0
- 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.3",
|
|
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.3",
|
|
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
|
|
|
@@ -420,6 +421,7 @@ function createClient(
|
|
|
420
421
|
apiKey: string,
|
|
421
422
|
extraBetas: string[],
|
|
422
423
|
stream: boolean,
|
|
424
|
+
extraHeaders?: Record<string, string>,
|
|
423
425
|
): { client: Anthropic; isOAuthToken: boolean } {
|
|
424
426
|
const oauthToken = isOAuthToken(apiKey);
|
|
425
427
|
|
|
@@ -438,7 +440,7 @@ function createClient(
|
|
|
438
440
|
isOAuth: oauthToken,
|
|
439
441
|
extraBetas: mergedBetas,
|
|
440
442
|
stream,
|
|
441
|
-
modelHeaders: model.headers,
|
|
443
|
+
modelHeaders: { ...(model.headers ?? {}), ...(extraHeaders ?? {}) },
|
|
442
444
|
});
|
|
443
445
|
|
|
444
446
|
const clientOptions: ConstructorParameters<typeof Anthropic>[0] = {
|
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
|
}
|
|
@@ -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
|
}
|
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
|
}
|