@oh-my-pi/pi-ai 13.13.0 → 13.14.0
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/CHANGELOG.md +17 -0
- package/package.json +2 -2
- package/src/model-thinking.ts +19 -2
- package/src/models.json +144 -8
- package/src/provider-models/openai-compat.ts +14 -1
- package/src/providers/openai-codex-responses.ts +1 -1
- package/src/providers/openai-responses-shared.ts +5 -1
- package/src/providers/openai-responses.ts +72 -10
- package/src/providers/transform-messages.ts +67 -63
- package/src/stream.ts +4 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [13.14.0] - 2026-03-20
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Fixed resumed OpenAI Responses sessions to avoid replaying stale same-provider native history on the first follow-up after process restart ([#488](https://github.com/can1357/oh-my-pi/issues/488))
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- Added bundled GPT-5.4 mini model metadata for OpenAI, OpenAI Codex, and GitHub Copilot, including low-to-xhigh thinking support and GitHub Copilot premium multiplier metadata
|
|
14
|
+
- Added bundled GPT-5.4 nano model metadata for OpenAI and OpenAI Codex, including low-to-xhigh thinking support
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
## [13.13.2] - 2026-03-18
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- Modified tool result handling for aborted assistant messages to preserve existing tool results when already recorded, instead of always replacing them with synthetic 'aborted' results
|
|
21
|
+
|
|
5
22
|
## [13.13.0] - 2026-03-18
|
|
6
23
|
### Changed
|
|
7
24
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-ai",
|
|
4
|
-
"version": "13.
|
|
4
|
+
"version": "13.14.0",
|
|
5
5
|
"description": "Unified LLM API with automatic model discovery and provider configuration",
|
|
6
6
|
"homepage": "https://github.com/can1357/oh-my-pi",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"@aws-sdk/client-bedrock-runtime": "^3",
|
|
42
42
|
"@bufbuild/protobuf": "^2.11",
|
|
43
43
|
"@google/genai": "^1.43",
|
|
44
|
-
"@oh-my-pi/pi-utils": "13.
|
|
44
|
+
"@oh-my-pi/pi-utils": "13.14.0",
|
|
45
45
|
"@sinclair/typebox": "^0.34",
|
|
46
46
|
"@smithy/node-http-handler": "^4.4",
|
|
47
47
|
"ajv": "^8.18",
|
package/src/model-thinking.ts
CHANGED
|
@@ -296,6 +296,12 @@ function applyOpenAICatalogPolicy(model: ApiModel<Api>, parsedModel: OpenAIModel
|
|
|
296
296
|
// Codex models: 400K figure includes output budget; input window is 272K.
|
|
297
297
|
if (parsedModel.variant.startsWith("codex") && parsedModel.variant !== "codex-spark") {
|
|
298
298
|
model.contextWindow = 272000;
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
// GPT-5.4 mini/nano use plain OpenAI IDs on the Codex transport, but Codex still
|
|
302
|
+
// enforces the lower prompt budget for these variants.
|
|
303
|
+
if (model.api === "openai-codex-responses" && (model.id === "gpt-5.4-mini" || model.id === "gpt-5.4-nano")) {
|
|
304
|
+
model.contextWindow = 272000;
|
|
299
305
|
}
|
|
300
306
|
}
|
|
301
307
|
|
|
@@ -374,7 +380,10 @@ function inferAnthropicSupportedEfforts<TApi extends Api>(
|
|
|
374
380
|
parsedModel: AnthropicModel,
|
|
375
381
|
model: ApiModel<TApi>,
|
|
376
382
|
): readonly Effort[] {
|
|
377
|
-
if (
|
|
383
|
+
if (
|
|
384
|
+
(model.api === "anthropic-messages" || model.api === "bedrock-converse-stream") &&
|
|
385
|
+
semverGte(parsedModel.version, "4.6")
|
|
386
|
+
) {
|
|
378
387
|
return parsedModel.kind === "opus" ? DEFAULT_REASONING_EFFORTS_WITH_XHIGH : DEFAULT_REASONING_EFFORTS;
|
|
379
388
|
}
|
|
380
389
|
return inferFallbackEfforts(model);
|
|
@@ -427,6 +436,14 @@ function inferThinkingControlMode<TApi extends Api>(
|
|
|
427
436
|
return "budget";
|
|
428
437
|
|
|
429
438
|
case "bedrock-converse-stream":
|
|
439
|
+
if (parsedModel.family === "anthropic") {
|
|
440
|
+
if (semverGte(parsedModel.version, "4.6") && parsedModel.kind === "opus") {
|
|
441
|
+
return "anthropic-adaptive";
|
|
442
|
+
}
|
|
443
|
+
if (semverGte(parsedModel.version, "4.5")) {
|
|
444
|
+
return "anthropic-budget-effort";
|
|
445
|
+
}
|
|
446
|
+
}
|
|
430
447
|
return "budget";
|
|
431
448
|
|
|
432
449
|
default:
|
|
@@ -460,7 +477,7 @@ function parseGeminiModel(modelId: string): GeminiModel | null {
|
|
|
460
477
|
}
|
|
461
478
|
|
|
462
479
|
function parseAnthropicModel(modelId: string): AnthropicModel | null {
|
|
463
|
-
const match = /claude-(opus|sonnet)-(\d
|
|
480
|
+
const match = /claude-(opus|sonnet)-(\d{1,2}(?:[.-]\d{1,2}){0,2})\b/.exec(modelId);
|
|
464
481
|
if (!match) {
|
|
465
482
|
return null;
|
|
466
483
|
}
|
package/src/models.json
CHANGED
|
@@ -345,9 +345,9 @@
|
|
|
345
345
|
"contextWindow": 1000000,
|
|
346
346
|
"maxTokens": 128000,
|
|
347
347
|
"thinking": {
|
|
348
|
-
"mode": "
|
|
348
|
+
"mode": "anthropic-adaptive",
|
|
349
349
|
"minLevel": "minimal",
|
|
350
|
-
"maxLevel": "
|
|
350
|
+
"maxLevel": "xhigh"
|
|
351
351
|
}
|
|
352
352
|
},
|
|
353
353
|
"cohere.command-r-plus-v1:0": {
|
|
@@ -720,9 +720,9 @@
|
|
|
720
720
|
"contextWindow": 1000000,
|
|
721
721
|
"maxTokens": 128000,
|
|
722
722
|
"thinking": {
|
|
723
|
-
"mode": "
|
|
723
|
+
"mode": "anthropic-adaptive",
|
|
724
724
|
"minLevel": "minimal",
|
|
725
|
-
"maxLevel": "
|
|
725
|
+
"maxLevel": "xhigh"
|
|
726
726
|
}
|
|
727
727
|
},
|
|
728
728
|
"eu.anthropic.claude-sonnet-4-20250514-v1:0": {
|
|
@@ -890,9 +890,9 @@
|
|
|
890
890
|
"contextWindow": 1000000,
|
|
891
891
|
"maxTokens": 128000,
|
|
892
892
|
"thinking": {
|
|
893
|
-
"mode": "
|
|
893
|
+
"mode": "anthropic-adaptive",
|
|
894
894
|
"minLevel": "minimal",
|
|
895
|
-
"maxLevel": "
|
|
895
|
+
"maxLevel": "xhigh"
|
|
896
896
|
}
|
|
897
897
|
},
|
|
898
898
|
"global.anthropic.claude-sonnet-4-20250514-v1:0": {
|
|
@@ -1846,9 +1846,9 @@
|
|
|
1846
1846
|
"contextWindow": 1000000,
|
|
1847
1847
|
"maxTokens": 128000,
|
|
1848
1848
|
"thinking": {
|
|
1849
|
-
"mode": "
|
|
1849
|
+
"mode": "anthropic-adaptive",
|
|
1850
1850
|
"minLevel": "minimal",
|
|
1851
|
-
"maxLevel": "
|
|
1851
|
+
"maxLevel": "xhigh"
|
|
1852
1852
|
}
|
|
1853
1853
|
},
|
|
1854
1854
|
"us.anthropic.claude-sonnet-4-20250514-v1:0": {
|
|
@@ -5085,6 +5085,38 @@
|
|
|
5085
5085
|
"maxLevel": "xhigh"
|
|
5086
5086
|
}
|
|
5087
5087
|
},
|
|
5088
|
+
"gpt-5.4-mini": {
|
|
5089
|
+
"id": "gpt-5.4-mini",
|
|
5090
|
+
"name": "GPT-5.4-mini",
|
|
5091
|
+
"api": "openai-responses",
|
|
5092
|
+
"provider": "github-copilot",
|
|
5093
|
+
"baseUrl": "https://api.individual.githubcopilot.com",
|
|
5094
|
+
"reasoning": true,
|
|
5095
|
+
"input": [
|
|
5096
|
+
"text",
|
|
5097
|
+
"image"
|
|
5098
|
+
],
|
|
5099
|
+
"cost": {
|
|
5100
|
+
"input": 0,
|
|
5101
|
+
"output": 0,
|
|
5102
|
+
"cacheRead": 0,
|
|
5103
|
+
"cacheWrite": 0
|
|
5104
|
+
},
|
|
5105
|
+
"contextWindow": 400000,
|
|
5106
|
+
"maxTokens": 128000,
|
|
5107
|
+
"headers": {
|
|
5108
|
+
"User-Agent": "GitHubCopilotChat/0.35.0",
|
|
5109
|
+
"Editor-Version": "vscode/1.107.0",
|
|
5110
|
+
"Editor-Plugin-Version": "copilot-chat/0.35.0",
|
|
5111
|
+
"Copilot-Integration-Id": "vscode-chat"
|
|
5112
|
+
},
|
|
5113
|
+
"thinking": {
|
|
5114
|
+
"mode": "effort",
|
|
5115
|
+
"minLevel": "low",
|
|
5116
|
+
"maxLevel": "xhigh"
|
|
5117
|
+
},
|
|
5118
|
+
"premiumMultiplier": 0.33
|
|
5119
|
+
},
|
|
5088
5120
|
"grok-code-fast-1": {
|
|
5089
5121
|
"id": "grok-code-fast-1",
|
|
5090
5122
|
"name": "Grok Code Fast 1",
|
|
@@ -29668,6 +29700,56 @@
|
|
|
29668
29700
|
"maxLevel": "xhigh"
|
|
29669
29701
|
}
|
|
29670
29702
|
},
|
|
29703
|
+
"gpt-5.4-mini": {
|
|
29704
|
+
"id": "gpt-5.4-mini",
|
|
29705
|
+
"name": "GPT-5.4 Mini",
|
|
29706
|
+
"api": "openai-responses",
|
|
29707
|
+
"provider": "openai",
|
|
29708
|
+
"baseUrl": "https://api.openai.com/v1",
|
|
29709
|
+
"reasoning": true,
|
|
29710
|
+
"input": [
|
|
29711
|
+
"text",
|
|
29712
|
+
"image"
|
|
29713
|
+
],
|
|
29714
|
+
"cost": {
|
|
29715
|
+
"input": 0.75,
|
|
29716
|
+
"output": 4.5,
|
|
29717
|
+
"cacheRead": 0.075,
|
|
29718
|
+
"cacheWrite": 0
|
|
29719
|
+
},
|
|
29720
|
+
"contextWindow": 400000,
|
|
29721
|
+
"maxTokens": 128000,
|
|
29722
|
+
"thinking": {
|
|
29723
|
+
"mode": "effort",
|
|
29724
|
+
"minLevel": "low",
|
|
29725
|
+
"maxLevel": "xhigh"
|
|
29726
|
+
}
|
|
29727
|
+
},
|
|
29728
|
+
"gpt-5.4-nano": {
|
|
29729
|
+
"id": "gpt-5.4-nano",
|
|
29730
|
+
"name": "GPT-5.4 Nano",
|
|
29731
|
+
"api": "openai-responses",
|
|
29732
|
+
"provider": "openai",
|
|
29733
|
+
"baseUrl": "https://api.openai.com/v1",
|
|
29734
|
+
"reasoning": true,
|
|
29735
|
+
"input": [
|
|
29736
|
+
"text",
|
|
29737
|
+
"image"
|
|
29738
|
+
],
|
|
29739
|
+
"cost": {
|
|
29740
|
+
"input": 0.2,
|
|
29741
|
+
"output": 1.25,
|
|
29742
|
+
"cacheRead": 0.02,
|
|
29743
|
+
"cacheWrite": 0
|
|
29744
|
+
},
|
|
29745
|
+
"contextWindow": 400000,
|
|
29746
|
+
"maxTokens": 128000,
|
|
29747
|
+
"thinking": {
|
|
29748
|
+
"mode": "effort",
|
|
29749
|
+
"minLevel": "low",
|
|
29750
|
+
"maxLevel": "xhigh"
|
|
29751
|
+
}
|
|
29752
|
+
},
|
|
29671
29753
|
"gpt-5.4-pro": {
|
|
29672
29754
|
"id": "gpt-5.4-pro",
|
|
29673
29755
|
"name": "GPT-5.4 Pro",
|
|
@@ -30220,6 +30302,60 @@
|
|
|
30220
30302
|
"minLevel": "low",
|
|
30221
30303
|
"maxLevel": "xhigh"
|
|
30222
30304
|
}
|
|
30305
|
+
},
|
|
30306
|
+
"gpt-5.4-mini": {
|
|
30307
|
+
"id": "gpt-5.4-mini",
|
|
30308
|
+
"name": "GPT-5.4 Mini",
|
|
30309
|
+
"api": "openai-codex-responses",
|
|
30310
|
+
"provider": "openai-codex",
|
|
30311
|
+
"baseUrl": "https://chatgpt.com/backend-api",
|
|
30312
|
+
"reasoning": true,
|
|
30313
|
+
"input": [
|
|
30314
|
+
"text",
|
|
30315
|
+
"image"
|
|
30316
|
+
],
|
|
30317
|
+
"cost": {
|
|
30318
|
+
"input": 0,
|
|
30319
|
+
"output": 0,
|
|
30320
|
+
"cacheRead": 0,
|
|
30321
|
+
"cacheWrite": 0
|
|
30322
|
+
},
|
|
30323
|
+
"contextWindow": 272000,
|
|
30324
|
+
"maxTokens": 128000,
|
|
30325
|
+
"preferWebsockets": true,
|
|
30326
|
+
"priority": 1,
|
|
30327
|
+
"thinking": {
|
|
30328
|
+
"mode": "effort",
|
|
30329
|
+
"minLevel": "low",
|
|
30330
|
+
"maxLevel": "xhigh"
|
|
30331
|
+
}
|
|
30332
|
+
},
|
|
30333
|
+
"gpt-5.4-nano": {
|
|
30334
|
+
"id": "gpt-5.4-nano",
|
|
30335
|
+
"name": "GPT-5.4 Nano",
|
|
30336
|
+
"api": "openai-codex-responses",
|
|
30337
|
+
"provider": "openai-codex",
|
|
30338
|
+
"baseUrl": "https://chatgpt.com/backend-api",
|
|
30339
|
+
"reasoning": true,
|
|
30340
|
+
"input": [
|
|
30341
|
+
"text",
|
|
30342
|
+
"image"
|
|
30343
|
+
],
|
|
30344
|
+
"cost": {
|
|
30345
|
+
"input": 0,
|
|
30346
|
+
"output": 0,
|
|
30347
|
+
"cacheRead": 0,
|
|
30348
|
+
"cacheWrite": 0
|
|
30349
|
+
},
|
|
30350
|
+
"contextWindow": 272000,
|
|
30351
|
+
"maxTokens": 128000,
|
|
30352
|
+
"preferWebsockets": true,
|
|
30353
|
+
"priority": 2,
|
|
30354
|
+
"thinking": {
|
|
30355
|
+
"mode": "effort",
|
|
30356
|
+
"minLevel": "low",
|
|
30357
|
+
"maxLevel": "xhigh"
|
|
30358
|
+
}
|
|
30223
30359
|
}
|
|
30224
30360
|
},
|
|
30225
30361
|
"opencode": {
|
|
@@ -173,13 +173,26 @@ function createBundledReferenceMap<TApi extends Api>(
|
|
|
173
173
|
return references;
|
|
174
174
|
}
|
|
175
175
|
|
|
176
|
+
function shouldReplaceGlobalReference(existing: Model<Api> | undefined, candidate: Model<Api>): boolean {
|
|
177
|
+
if (!existing) return true;
|
|
178
|
+
if (candidate.contextWindow !== existing.contextWindow) {
|
|
179
|
+
return candidate.contextWindow > existing.contextWindow;
|
|
180
|
+
}
|
|
181
|
+
if (candidate.maxTokens !== existing.maxTokens) {
|
|
182
|
+
return candidate.maxTokens > existing.maxTokens;
|
|
183
|
+
}
|
|
184
|
+
// When limits tie, prefer OpenAI as the canonical reference so generic OpenAI-family
|
|
185
|
+
// providers inherit OpenAI pricing/capabilities instead of Copilot-specific metadata.
|
|
186
|
+
return existing.provider !== "openai" && candidate.provider === "openai";
|
|
187
|
+
}
|
|
188
|
+
|
|
176
189
|
function createGlobalReferenceMap(): Map<string, Model<Api>> {
|
|
177
190
|
const references = new Map<string, Model<Api>>();
|
|
178
191
|
for (const provider of getBundledProviders()) {
|
|
179
192
|
for (const model of getBundledModels(provider as Parameters<typeof getBundledModels>[0])) {
|
|
180
193
|
const candidate = model as Model<Api>;
|
|
181
194
|
const existing = references.get(candidate.id);
|
|
182
|
-
if (
|
|
195
|
+
if (shouldReplaceGlobalReference(existing, candidate)) {
|
|
183
196
|
references.set(candidate.id, candidate);
|
|
184
197
|
}
|
|
185
198
|
}
|
|
@@ -2134,7 +2134,7 @@ function convertMessages(model: Model<"openai-codex-responses">, context: Contex
|
|
|
2134
2134
|
} satisfies ResponseOutputMessage);
|
|
2135
2135
|
continue;
|
|
2136
2136
|
}
|
|
2137
|
-
if (block.type === "toolCall"
|
|
2137
|
+
if (block.type === "toolCall") {
|
|
2138
2138
|
const toolCall = block as ToolCall;
|
|
2139
2139
|
const normalized = normalizeResponsesToolCallId(toolCall.id);
|
|
2140
2140
|
outputItems.push({
|
|
@@ -102,6 +102,7 @@ export function convertResponsesAssistantMessage<TApi extends Api>(
|
|
|
102
102
|
model: Model<TApi>,
|
|
103
103
|
msgIndex: number,
|
|
104
104
|
knownCallIds: Set<string>,
|
|
105
|
+
includeThinkingSignatures = true,
|
|
105
106
|
): ResponseInput {
|
|
106
107
|
const outputItems: ResponseInput = [];
|
|
107
108
|
const isDifferentModel =
|
|
@@ -109,6 +110,9 @@ export function convertResponsesAssistantMessage<TApi extends Api>(
|
|
|
109
110
|
|
|
110
111
|
for (const block of assistantMsg.content) {
|
|
111
112
|
if (block.type === "thinking" && assistantMsg.stopReason !== "error") {
|
|
113
|
+
if (!includeThinkingSignatures) {
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
112
116
|
if (block.thinkingSignature) {
|
|
113
117
|
outputItems.push(JSON.parse(block.thinkingSignature) as ResponseReasoningItem);
|
|
114
118
|
}
|
|
@@ -134,7 +138,7 @@ export function convertResponsesAssistantMessage<TApi extends Api>(
|
|
|
134
138
|
continue;
|
|
135
139
|
}
|
|
136
140
|
|
|
137
|
-
if (block.type !== "toolCall"
|
|
141
|
+
if (block.type !== "toolCall") {
|
|
138
142
|
continue;
|
|
139
143
|
}
|
|
140
144
|
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
isSpecialServiceTier,
|
|
15
15
|
type MessageAttribution,
|
|
16
16
|
type Model,
|
|
17
|
+
type ProviderSessionState,
|
|
17
18
|
type ServiceTier,
|
|
18
19
|
type StreamFunction,
|
|
19
20
|
type StreamOptions,
|
|
@@ -74,6 +75,45 @@ export interface OpenAIResponsesOptions extends StreamOptions {
|
|
|
74
75
|
strictResponsesPairing?: boolean;
|
|
75
76
|
}
|
|
76
77
|
|
|
78
|
+
const OPENAI_RESPONSES_PROVIDER_SESSION_STATE_PREFIX = "openai-responses:";
|
|
79
|
+
|
|
80
|
+
interface OpenAIResponsesProviderSessionState extends ProviderSessionState {
|
|
81
|
+
nativeHistoryReplayWarmed: boolean;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function createOpenAIResponsesProviderSessionState(): OpenAIResponsesProviderSessionState {
|
|
85
|
+
const state: OpenAIResponsesProviderSessionState = {
|
|
86
|
+
nativeHistoryReplayWarmed: false,
|
|
87
|
+
close: () => {
|
|
88
|
+
state.nativeHistoryReplayWarmed = false;
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
return state;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function getOpenAIResponsesProviderSessionStateKey(model: Model<"openai-responses">): string {
|
|
95
|
+
return `${OPENAI_RESPONSES_PROVIDER_SESSION_STATE_PREFIX}${model.provider}`;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function getOpenAIResponsesProviderSessionState(
|
|
99
|
+
model: Model<"openai-responses">,
|
|
100
|
+
providerSessionState: Map<string, ProviderSessionState> | undefined,
|
|
101
|
+
): OpenAIResponsesProviderSessionState | undefined {
|
|
102
|
+
if (!providerSessionState) return undefined;
|
|
103
|
+
const key = getOpenAIResponsesProviderSessionStateKey(model);
|
|
104
|
+
const existing = providerSessionState.get(key) as OpenAIResponsesProviderSessionState | undefined;
|
|
105
|
+
if (existing) return existing;
|
|
106
|
+
const created = createOpenAIResponsesProviderSessionState();
|
|
107
|
+
providerSessionState.set(key, created);
|
|
108
|
+
return created;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function canReplayOpenAIResponsesNativeHistory(
|
|
112
|
+
providerSessionState: OpenAIResponsesProviderSessionState | undefined,
|
|
113
|
+
): boolean {
|
|
114
|
+
return providerSessionState?.nativeHistoryReplayWarmed ?? true;
|
|
115
|
+
}
|
|
116
|
+
|
|
77
117
|
type OpenAIResponsesSamplingParams = ResponseCreateParamsStreaming & {
|
|
78
118
|
top_p?: number;
|
|
79
119
|
top_k?: number;
|
|
@@ -126,7 +166,8 @@ export const streamOpenAIResponses: StreamFunction<"openai-responses"> = (
|
|
|
126
166
|
options?.headers,
|
|
127
167
|
options?.initiatorOverride,
|
|
128
168
|
);
|
|
129
|
-
const
|
|
169
|
+
const providerSessionState = getOpenAIResponsesProviderSessionState(model, options?.providerSessionState);
|
|
170
|
+
const { params } = buildParams(model, context, options, providerSessionState);
|
|
130
171
|
const requestAbortController = new AbortController();
|
|
131
172
|
const requestSignal = options?.signal
|
|
132
173
|
? AbortSignal.any([options.signal, requestAbortController.signal])
|
|
@@ -174,6 +215,7 @@ export const streamOpenAIResponses: StreamFunction<"openai-responses"> = (
|
|
|
174
215
|
}
|
|
175
216
|
|
|
176
217
|
output.providerPayload = createOpenAIResponsesHistoryPayload(model.provider, nativeOutputItems);
|
|
218
|
+
if (providerSessionState) providerSessionState.nativeHistoryReplayWarmed = true;
|
|
177
219
|
|
|
178
220
|
output.duration = Date.now() - startTime;
|
|
179
221
|
if (firstTokenTime) output.ttft = firstTokenTime - startTime;
|
|
@@ -246,12 +288,18 @@ function createClient(
|
|
|
246
288
|
function buildParams(
|
|
247
289
|
model: Model<"openai-responses">,
|
|
248
290
|
context: Context,
|
|
249
|
-
options
|
|
291
|
+
options: OpenAIResponsesOptions | undefined,
|
|
292
|
+
providerSessionState: OpenAIResponsesProviderSessionState | undefined,
|
|
250
293
|
): { conversationMessages: ResponseInput; params: OpenAIResponsesSamplingParams } {
|
|
251
294
|
const strictResponsesPairing =
|
|
252
295
|
options?.strictResponsesPairing ??
|
|
253
296
|
(isAzureOpenAIBaseUrl(model.baseUrl ?? "") || model.provider === "github-copilot");
|
|
254
|
-
const conversationMessages = convertConversationMessages(
|
|
297
|
+
const conversationMessages = convertConversationMessages(
|
|
298
|
+
model,
|
|
299
|
+
context,
|
|
300
|
+
strictResponsesPairing,
|
|
301
|
+
providerSessionState,
|
|
302
|
+
);
|
|
255
303
|
const messages: ResponseInput = [...conversationMessages];
|
|
256
304
|
|
|
257
305
|
if (context.systemPrompt) {
|
|
@@ -354,9 +402,11 @@ function convertConversationMessages(
|
|
|
354
402
|
model: Model<"openai-responses">,
|
|
355
403
|
context: Context,
|
|
356
404
|
strictResponsesPairing: boolean,
|
|
405
|
+
providerSessionState: OpenAIResponsesProviderSessionState | undefined,
|
|
357
406
|
): ResponseInput {
|
|
358
407
|
const messages: ResponseInput = [];
|
|
359
408
|
let knownCallIds = new Set<string>();
|
|
409
|
+
const shouldReplayNativeHistory = canReplayOpenAIResponsesNativeHistory(providerSessionState);
|
|
360
410
|
const transformedMessages = transformMessages(context.messages, model, normalizeResponsesToolCallIdForTransform);
|
|
361
411
|
|
|
362
412
|
let msgIndex = 0;
|
|
@@ -364,7 +414,15 @@ function convertConversationMessages(
|
|
|
364
414
|
if (msg.role === "user" || msg.role === "developer") {
|
|
365
415
|
const providerPayload = (msg as { providerPayload?: AssistantMessage["providerPayload"] }).providerPayload;
|
|
366
416
|
const historyItems = getOpenAIResponsesHistoryItems(providerPayload, model.provider);
|
|
367
|
-
|
|
417
|
+
const shouldReplayPayloadItems =
|
|
418
|
+
shouldReplayNativeHistory ||
|
|
419
|
+
(historyItems?.some(item => {
|
|
420
|
+
if (!item || typeof item !== "object") return false;
|
|
421
|
+
const candidate = item as { type?: unknown };
|
|
422
|
+
return candidate.type === "compaction" || candidate.type === "compaction_summary";
|
|
423
|
+
}) ??
|
|
424
|
+
false);
|
|
425
|
+
if (historyItems && shouldReplayPayloadItems) {
|
|
368
426
|
messages.push(...sanitizeOpenAIResponsesHistoryItemsForReplay(historyItems));
|
|
369
427
|
knownCallIds = collectKnownCallIds(messages);
|
|
370
428
|
msgIndex++;
|
|
@@ -375,11 +433,9 @@ function convertConversationMessages(
|
|
|
375
433
|
messages.push({ role: "user", content });
|
|
376
434
|
} else if (msg.role === "assistant") {
|
|
377
435
|
const assistantMsg = msg as AssistantMessage;
|
|
378
|
-
const providerPayload =
|
|
379
|
-
assistantMsg.providerPayload,
|
|
380
|
-
|
|
381
|
-
assistantMsg.provider,
|
|
382
|
-
);
|
|
436
|
+
const providerPayload = shouldReplayNativeHistory
|
|
437
|
+
? getOpenAIResponsesHistoryPayload(assistantMsg.providerPayload, model.provider, assistantMsg.provider)
|
|
438
|
+
: undefined;
|
|
383
439
|
const historyItems = providerPayload?.items;
|
|
384
440
|
if (historyItems) {
|
|
385
441
|
const sanitizedHistoryItems = sanitizeOpenAIResponsesHistoryItemsForReplay(historyItems);
|
|
@@ -393,7 +449,13 @@ function convertConversationMessages(
|
|
|
393
449
|
continue;
|
|
394
450
|
}
|
|
395
451
|
|
|
396
|
-
const outputItems = convertResponsesAssistantMessage(
|
|
452
|
+
const outputItems = convertResponsesAssistantMessage(
|
|
453
|
+
assistantMsg,
|
|
454
|
+
model,
|
|
455
|
+
msgIndex,
|
|
456
|
+
knownCallIds,
|
|
457
|
+
shouldReplayNativeHistory,
|
|
458
|
+
);
|
|
397
459
|
if (outputItems.length === 0) continue;
|
|
398
460
|
messages.push(...outputItems);
|
|
399
461
|
} else if (msg.role === "toolResult") {
|
|
@@ -124,101 +124,105 @@ export function transformMessages<TApi extends Api>(
|
|
|
124
124
|
});
|
|
125
125
|
|
|
126
126
|
// Second pass: insert synthetic empty tool results for orphaned tool calls
|
|
127
|
-
//
|
|
127
|
+
// and preserve aborted/errored tool results when they were already persisted.
|
|
128
128
|
const result: Message[] = [];
|
|
129
129
|
let pendingToolCalls: ToolCall[] = [];
|
|
130
|
-
|
|
130
|
+
let pendingAbortedToolCalls = new Map<string, ToolCall>();
|
|
131
|
+
let pendingAbortedTimestamp: number | undefined;
|
|
132
|
+
// Track tool call status: whether resolved (has result) or aborted (synthetic result injected, skip later real results)
|
|
131
133
|
const toolCallStatus = new Map<string, ToolCallStatus>();
|
|
132
134
|
|
|
135
|
+
const flushPendingToolCalls = (timestamp: number): void => {
|
|
136
|
+
if (pendingToolCalls.length === 0) return;
|
|
137
|
+
for (const tc of pendingToolCalls) {
|
|
138
|
+
if (!toolCallStatus.has(tc.id)) {
|
|
139
|
+
result.push({
|
|
140
|
+
role: "toolResult",
|
|
141
|
+
toolCallId: tc.id,
|
|
142
|
+
toolName: tc.name,
|
|
143
|
+
content: [{ type: "text", text: "No result provided" }],
|
|
144
|
+
isError: true,
|
|
145
|
+
timestamp,
|
|
146
|
+
} as ToolResultMessage);
|
|
147
|
+
toolCallStatus.set(tc.id, ToolCallStatus.Resolved);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
pendingToolCalls = [];
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const flushPendingAbortedToolCalls = (): void => {
|
|
154
|
+
if (pendingAbortedTimestamp === undefined) return;
|
|
155
|
+
for (const tc of pendingAbortedToolCalls.values()) {
|
|
156
|
+
if (!toolCallStatus.has(tc.id)) {
|
|
157
|
+
result.push({
|
|
158
|
+
role: "toolResult",
|
|
159
|
+
toolCallId: tc.id,
|
|
160
|
+
toolName: tc.name,
|
|
161
|
+
content: [{ type: "text", text: "aborted" }],
|
|
162
|
+
isError: true,
|
|
163
|
+
timestamp: pendingAbortedTimestamp,
|
|
164
|
+
} as ToolResultMessage);
|
|
165
|
+
toolCallStatus.set(tc.id, ToolCallStatus.Aborted);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
result.push({
|
|
169
|
+
role: "developer",
|
|
170
|
+
content: turnAbortedGuidance,
|
|
171
|
+
timestamp: pendingAbortedTimestamp + 1,
|
|
172
|
+
} as DeveloperMessage);
|
|
173
|
+
pendingAbortedToolCalls = new Map();
|
|
174
|
+
pendingAbortedTimestamp = undefined;
|
|
175
|
+
};
|
|
176
|
+
|
|
133
177
|
for (let i = 0; i < transformed.length; i++) {
|
|
134
178
|
const msg = transformed[i];
|
|
179
|
+
const messageTimestamp = "timestamp" in msg && typeof msg.timestamp === "number" ? msg.timestamp : Date.now();
|
|
135
180
|
|
|
136
181
|
if (msg.role === "assistant") {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
for (const tc of pendingToolCalls) {
|
|
140
|
-
if (!toolCallStatus.has(tc.id)) {
|
|
141
|
-
result.push({
|
|
142
|
-
role: "toolResult",
|
|
143
|
-
toolCallId: tc.id,
|
|
144
|
-
toolName: tc.name,
|
|
145
|
-
content: [{ type: "text", text: "No result provided" }],
|
|
146
|
-
isError: true,
|
|
147
|
-
timestamp: Date.now(),
|
|
148
|
-
} as ToolResultMessage);
|
|
149
|
-
toolCallStatus.set(tc.id, ToolCallStatus.Resolved);
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
pendingToolCalls = [];
|
|
153
|
-
}
|
|
182
|
+
flushPendingToolCalls(messageTimestamp);
|
|
183
|
+
flushPendingAbortedToolCalls();
|
|
154
184
|
|
|
155
|
-
// For errored/aborted assistant messages: keep tool calls intact,
|
|
156
|
-
// inject synthetic "aborted" results, and add guidance marker.
|
|
157
|
-
// This preserves structure so the model knows what was attempted.
|
|
158
185
|
const assistantMsg = msg as AssistantMessage;
|
|
159
186
|
const toolCalls = assistantMsg.content.filter(b => b.type === "toolCall") as ToolCall[];
|
|
160
187
|
|
|
161
188
|
if (assistantMsg.stopReason === "error" || assistantMsg.stopReason === "aborted") {
|
|
162
|
-
//
|
|
189
|
+
// Keep the assistant message with tool calls intact. If real tool results follow, preserve them;
|
|
190
|
+
// otherwise synthesize aborted results before the next turn boundary.
|
|
163
191
|
result.push(msg);
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
for (const tc of toolCalls) {
|
|
167
|
-
toolCallStatus.set(tc.id, ToolCallStatus.Aborted);
|
|
168
|
-
result.push({
|
|
169
|
-
role: "toolResult",
|
|
170
|
-
toolCallId: tc.id,
|
|
171
|
-
toolName: tc.name,
|
|
172
|
-
content: [{ type: "text", text: "aborted" }],
|
|
173
|
-
isError: true,
|
|
174
|
-
timestamp: assistantMsg.timestamp,
|
|
175
|
-
} as ToolResultMessage);
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// Inject turn-aborted guidance marker as developer message
|
|
179
|
-
result.push({
|
|
180
|
-
role: "developer",
|
|
181
|
-
content: turnAbortedGuidance,
|
|
182
|
-
timestamp: assistantMsg.timestamp + 1,
|
|
183
|
-
} as DeveloperMessage);
|
|
184
|
-
|
|
192
|
+
pendingAbortedToolCalls = new Map(toolCalls.map(toolCall => [toolCall.id, toolCall] as const));
|
|
193
|
+
pendingAbortedTimestamp = assistantMsg.timestamp;
|
|
185
194
|
continue;
|
|
186
195
|
}
|
|
187
196
|
|
|
188
|
-
// Track tool calls from this normal assistant message
|
|
189
197
|
if (toolCalls.length > 0) {
|
|
190
198
|
pendingToolCalls = toolCalls;
|
|
191
199
|
}
|
|
192
200
|
|
|
193
201
|
result.push(msg);
|
|
194
202
|
} else if (msg.role === "toolResult") {
|
|
195
|
-
|
|
203
|
+
if (pendingAbortedToolCalls.has(msg.toolCallId)) {
|
|
204
|
+
pendingAbortedToolCalls.delete(msg.toolCallId);
|
|
205
|
+
toolCallStatus.set(msg.toolCallId, ToolCallStatus.Resolved);
|
|
206
|
+
result.push(msg);
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
|
|
196
210
|
if (toolCallStatus.get(msg.toolCallId) === ToolCallStatus.Aborted) continue;
|
|
197
211
|
toolCallStatus.set(msg.toolCallId, ToolCallStatus.Resolved);
|
|
198
212
|
result.push(msg);
|
|
199
213
|
} else if (msg.role === "user" || msg.role === "developer") {
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
for (const tc of pendingToolCalls) {
|
|
203
|
-
if (!toolCallStatus.has(tc.id)) {
|
|
204
|
-
result.push({
|
|
205
|
-
role: "toolResult",
|
|
206
|
-
toolCallId: tc.id,
|
|
207
|
-
toolName: tc.name,
|
|
208
|
-
content: [{ type: "text", text: "No result provided" }],
|
|
209
|
-
isError: true,
|
|
210
|
-
timestamp: Date.now(),
|
|
211
|
-
} as ToolResultMessage);
|
|
212
|
-
toolCallStatus.set(tc.id, ToolCallStatus.Resolved);
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
pendingToolCalls = [];
|
|
216
|
-
}
|
|
214
|
+
flushPendingToolCalls(messageTimestamp);
|
|
215
|
+
flushPendingAbortedToolCalls();
|
|
217
216
|
result.push(msg);
|
|
218
217
|
} else {
|
|
218
|
+
flushPendingToolCalls(messageTimestamp);
|
|
219
|
+
flushPendingAbortedToolCalls();
|
|
219
220
|
result.push(msg);
|
|
220
221
|
}
|
|
221
222
|
}
|
|
222
223
|
|
|
224
|
+
flushPendingToolCalls(Date.now());
|
|
225
|
+
flushPendingAbortedToolCalls();
|
|
226
|
+
|
|
223
227
|
return result;
|
|
224
228
|
}
|
package/src/stream.ts
CHANGED
|
@@ -502,6 +502,10 @@ function mapOptionsForApi<TApi extends Api>(
|
|
|
502
502
|
thinkingBudgets: options?.thinkingBudgets,
|
|
503
503
|
toolChoice: mapAnthropicToolChoice(options?.toolChoice),
|
|
504
504
|
};
|
|
505
|
+
// Adaptive mode sends effort directly, no budget_tokens — skip budget inflation.
|
|
506
|
+
if (model.thinking?.mode === "anthropic-adaptive") {
|
|
507
|
+
return castApi<"bedrock-converse-stream">(bedrockBase);
|
|
508
|
+
}
|
|
505
509
|
const budgetInfo = resolveBedrockThinkingBudget(model as Model<"bedrock-converse-stream">, options);
|
|
506
510
|
if (!budgetInfo) return bedrockBase as OptionsForApi<TApi>;
|
|
507
511
|
let maxTokens = bedrockBase.maxTokens ?? model.maxTokens;
|