@oh-my-pi/pi-ai 18.6.0 → 18.6.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/CHANGELOG.md +32 -38
- package/README.md +15 -0
- package/dist/types/auth/types.d.ts +4 -0
- package/dist/types/auth-broker/refresher.d.ts +6 -1
- package/dist/types/error/flags.d.ts +15 -0
- package/dist/types/providers/anthropic.d.ts +15 -2
- package/dist/types/providers/openai-responses.d.ts +32 -3
- package/dist/types/registry/oauth/snowflake.d.ts +3 -0
- package/dist/types/registry/snowflake.d.ts +4 -0
- package/dist/types/types.d.ts +40 -3
- package/dist/types/utils/idle-iterator.d.ts +11 -0
- package/dist/types/utils/stream-markup-healing.d.ts +4 -4
- package/dist/types/utils.d.ts +6 -0
- package/package.json +6 -6
- package/src/auth/cascade.ts +6 -2
- package/src/auth/health.ts +1 -0
- package/src/auth/select.ts +3 -1
- package/src/auth/types.ts +4 -0
- package/src/auth-broker/refresher.ts +8 -1
- package/src/dialect/deepseek.ts +24 -3
- package/src/error/flags.ts +22 -0
- package/src/providers/anthropic.ts +111 -45
- package/src/providers/devin.ts +3 -1
- package/src/providers/factory-droid.ts +2 -0
- package/src/providers/gitlab-duo.ts +1 -0
- package/src/providers/openai-anthropic-shim.ts +1 -0
- package/src/providers/openai-codex-responses.ts +14 -11
- package/src/providers/openai-completions.ts +3 -0
- package/src/providers/openai-responses.ts +349 -6
- package/src/providers/openai-shared.ts +39 -16
- package/src/providers/pi-native-server.ts +1 -0
- package/src/registry/hooks/custom.ts +2 -0
- package/src/registry/oauth/snowflake.ts +183 -0
- package/src/registry/registry.ts +2 -0
- package/src/registry/snowflake.ts +149 -0
- package/src/stream.ts +35 -33
- package/src/types.ts +41 -3
- package/src/usage/claude.ts +3 -0
- package/src/utils/idle-iterator.ts +80 -35
- package/src/utils/stream-markup-healing.ts +4 -4
- package/src/utils/thinking-loop.ts +18 -10
- package/src/utils.ts +19 -4
package/src/error/flags.ts
CHANGED
|
@@ -187,6 +187,25 @@ export function isResponsesRequestBodyReadTimeout(message: {
|
|
|
187
187
|
);
|
|
188
188
|
}
|
|
189
189
|
|
|
190
|
+
/**
|
|
191
|
+
* Codex code for the experimental native turn lane refusing a mid-response
|
|
192
|
+
* `response.steer`; the server then drops the in-flight response and closes
|
|
193
|
+
* the socket.
|
|
194
|
+
*/
|
|
195
|
+
export const CODEX_NATIVE_LANE_STEER_REJECTED_CODE = "unsupported_native_inflight_message";
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* A Codex turn the native turn lane dropped because omp steered it. The
|
|
199
|
+
* rejection answers our own `response.steer`, not the model's health: the
|
|
200
|
+
* provider stops steering the session, so the same model replays cleanly.
|
|
201
|
+
*/
|
|
202
|
+
export function isCodexSteerRejection(message: { api?: Api; errorMessage?: string }): boolean {
|
|
203
|
+
return (
|
|
204
|
+
message.api === "openai-codex-responses" &&
|
|
205
|
+
message.errorMessage?.includes(`code=${CODEX_NATIVE_LANE_STEER_REJECTED_CODE}`) === true
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
|
|
190
209
|
export const TRANSIENT_TRANSPORT_PATTERN =
|
|
191
210
|
/\b(?:no[_ -]?capacity|(?:high|peak)[ _-]?demand|(?:at|over|insufficient)[ _-]?capacity|capacity[ _-]?(?:exceeded|exhausted)|peak[ _-]?load)\b|overloaded|provider.?returned.?error|rate.?limit|too many requests|auth-gateway\s+5\d{2}(?=[:\s]|$)|\b(?:429|500|502|503|504)\b|service.?unavailable|server.?error|internal.?error|retry your request|network.?error|connection.?error|connection.?refused|unable.?to.?connect\.\s*is the computer able to access the url\?|other side closed|fetch failed|upstream.?connect|upstream.?request.?failed|reset before headers|socket hang up|timed? out|timeout|terminated|retry delay|stream stall|no error details in response|HTTP2(?:StreamReset|RefusedStream|EnhanceYourCalm)|nghttp2_(?:internal_error|refused_stream)|stream closed with error code nghttp2_(?:internal_error|refused_stream)|malformed.?function.?call/i;
|
|
192
211
|
const AUTH_FAILURE_PATTERN =
|
|
@@ -586,6 +605,9 @@ function classifyText(
|
|
|
586
605
|
if ((api === "openai-responses" || api === "openai-codex-responses") && isStaleResponsesText(errorMessage)) {
|
|
587
606
|
kinds |= Flag.StaleResponsesItem;
|
|
588
607
|
}
|
|
608
|
+
// Retryable like the provider's own classification of the code, so a
|
|
609
|
+
// message reclassified from its text alone still retries.
|
|
610
|
+
if (isCodexSteerRejection({ api, errorMessage })) kinds |= Flag.Transient;
|
|
589
611
|
|
|
590
612
|
// Fireworks mid-generation NaN 400 is a model-side decode fault, not a bad
|
|
591
613
|
// request; a byte-identical replay succeeds, so treat it as transient.
|
|
@@ -3,6 +3,7 @@ import { scheduler } from "node:timers/promises";
|
|
|
3
3
|
import * as tls from "node:tls";
|
|
4
4
|
import { isAnthropicSigningProxyUrl, isOfficialAnthropicApiUrl } from "@oh-my-pi/pi-catalog/compat/anthropic";
|
|
5
5
|
import { hostMatchesUrl, isVertexRawPredictUrl } from "@oh-my-pi/pi-catalog/hosts";
|
|
6
|
+
import type { Effort } from "@oh-my-pi/pi-catalog/effort";
|
|
6
7
|
import { mapEffortToAnthropicAdaptiveEffort } from "@oh-my-pi/pi-catalog/model-thinking";
|
|
7
8
|
import { calculateCost, getBundledModel } from "@oh-my-pi/pi-catalog/models";
|
|
8
9
|
import { isAnthropicOAuthToken } from "@oh-my-pi/pi-catalog/utils";
|
|
@@ -21,7 +22,9 @@ import * as AIError from "../error";
|
|
|
21
22
|
import { parseToolCallArguments } from "../utils/tool-call-arguments";
|
|
22
23
|
import { getEnvApiKey, OUTPUT_FALLBACK_BUFFER } from "../stream";
|
|
23
24
|
import type {
|
|
25
|
+
AnthropicCompactionFiles,
|
|
24
26
|
AnthropicCompactionPayload,
|
|
27
|
+
AnthropicCompactionRequest,
|
|
25
28
|
AnthropicFallbackContent,
|
|
26
29
|
AnthropicMessagePayload,
|
|
27
30
|
AnthropicOutputEffort,
|
|
@@ -646,7 +649,7 @@ const CLAUDE_BILLING_HEADER_PREFIX = "x-anthropic-billing-header:";
|
|
|
646
649
|
function createClaudeBillingHeader(firstUserMessageText: string): string {
|
|
647
650
|
// Fingerprint: SHA256(salt + msg[4] + msg[7] + msg[20] + version)[:3]
|
|
648
651
|
// Matches CC's computeFingerprint in utils/fingerprint.ts.
|
|
649
|
-
// Uses chars from the first user
|
|
652
|
+
// Uses chars from the first conversational wire user turn (not the system prompt).
|
|
650
653
|
const k = [4, 7, 20].map(i => firstUserMessageText[i] ?? "0").join("");
|
|
651
654
|
const version = getClaudeCodeVersion();
|
|
652
655
|
const versionSuffix = Bun.SHA256.hash(`59cf53e54c78${k}${version}`, "hex").slice(0, 3);
|
|
@@ -3498,7 +3501,7 @@ export type AnthropicSystemBlock = {
|
|
|
3498
3501
|
type SystemBlockOptions = {
|
|
3499
3502
|
includeClaudeCodeInstruction?: boolean;
|
|
3500
3503
|
extraInstructions?: string[];
|
|
3501
|
-
/** Text of the first user
|
|
3504
|
+
/** Text of the first conversational wire user turn, used as the billing fingerprint seed. */
|
|
3502
3505
|
firstUserMessageText?: string;
|
|
3503
3506
|
/** Cache lifetime shared by the OAuth system breakpoint and later message breakpoints. */
|
|
3504
3507
|
cacheControl?: AnthropicCacheControl;
|
|
@@ -4185,16 +4188,19 @@ function resolveAnthropicAdaptiveEffort(
|
|
|
4185
4188
|
return mapEffortToAnthropicAdaptiveEffort(model, requestedEffort);
|
|
4186
4189
|
}
|
|
4187
4190
|
|
|
4188
|
-
function
|
|
4191
|
+
function extractClaudeCodeFirstWireUserMessageText(messages: readonly Message[]): string {
|
|
4189
4192
|
for (const message of messages) {
|
|
4190
|
-
if (message.role !== "user") continue;
|
|
4193
|
+
if ((message.role !== "user" && message.role !== "developer") || anthropicControlOf(message)) continue;
|
|
4191
4194
|
const { content } = message;
|
|
4192
|
-
if (typeof content === "string")
|
|
4193
|
-
|
|
4195
|
+
if (typeof content === "string") {
|
|
4196
|
+
if (content.trim()) return content;
|
|
4197
|
+
continue;
|
|
4198
|
+
}
|
|
4199
|
+
if (!Array.isArray(content)) continue;
|
|
4194
4200
|
for (const block of content) {
|
|
4195
|
-
if (block.type === "text") return block.text;
|
|
4201
|
+
if (block.type === "text" && block.text.trim()) return block.text;
|
|
4196
4202
|
}
|
|
4197
|
-
return "";
|
|
4203
|
+
if (content.some(block => block.type === "image")) return "";
|
|
4198
4204
|
}
|
|
4199
4205
|
return "";
|
|
4200
4206
|
}
|
|
@@ -4306,11 +4312,16 @@ function diffAnthropicActiveTools(
|
|
|
4306
4312
|
* at the tail. Records at a different index than they were written at are
|
|
4307
4313
|
* rewritten history: they keep the declaration but emit no controls, so the
|
|
4308
4314
|
* net change from the declared baseline lands at the first live position.
|
|
4315
|
+
* Without `ownChange` (an on-demand compaction request, which ends inside the
|
|
4316
|
+
* conversation) the tail control is left out: there it would sit between the
|
|
4317
|
+
* summarized prefix and the retained turns, where live requests never sent
|
|
4318
|
+
* it, and invalidate their signed thinking. The next live turn sends it.
|
|
4309
4319
|
*/
|
|
4310
4320
|
function planAnthropicToolControls(
|
|
4311
4321
|
context: Context,
|
|
4312
4322
|
records: readonly AnthropicControlRecord[],
|
|
4313
4323
|
enabled: boolean,
|
|
4324
|
+
ownChange: boolean,
|
|
4314
4325
|
): { tools: Tool[] | undefined; inserts: AnthropicControlInsert[]; record: AnthropicToolControls | undefined } {
|
|
4315
4326
|
if (!enabled || !context.tools) return { tools: context.tools, inserts: [], record: undefined };
|
|
4316
4327
|
const definitions = new Map<string, Tool>();
|
|
@@ -4353,7 +4364,7 @@ function planAnthropicToolControls(
|
|
|
4353
4364
|
if (toolChanges.length > 0) inserts.push({ index: record.index, spec: { toolChanges } });
|
|
4354
4365
|
previous = record.tools.active;
|
|
4355
4366
|
}
|
|
4356
|
-
const toolChanges = diffAnthropicActiveTools(previous, activeNames, declaredSet);
|
|
4367
|
+
const toolChanges = ownChange ? diffAnthropicActiveTools(previous, activeNames, declaredSet) : [];
|
|
4357
4368
|
if (toolChanges.length > 0) inserts.push({ index: context.messages.length, spec: { toolChanges } });
|
|
4358
4369
|
}
|
|
4359
4370
|
|
|
@@ -4380,7 +4391,8 @@ function planAnthropicToolControls(
|
|
|
4380
4391
|
* restore, so a request without an explicit effort keeps the level in force.
|
|
4381
4392
|
* Records at a different index than they were written at are rewritten history:
|
|
4382
4393
|
* they keep the top-level effort but emit no controls, so the net change lands
|
|
4383
|
-
* at the first live position.
|
|
4394
|
+
* at the first live position. Without `ownChange` (an on-demand compaction
|
|
4395
|
+
* request) `current` is not applied: the request runs at the effort in force.
|
|
4384
4396
|
*/
|
|
4385
4397
|
function planAnthropicEffortControls(
|
|
4386
4398
|
current: AnthropicOutputEffort | undefined,
|
|
@@ -4388,6 +4400,7 @@ function planAnthropicEffortControls(
|
|
|
4388
4400
|
records: readonly AnthropicControlRecord[],
|
|
4389
4401
|
enabled: boolean,
|
|
4390
4402
|
compactionReplay: AnthropicCompactionReplay | undefined,
|
|
4403
|
+
ownChange: boolean,
|
|
4391
4404
|
): {
|
|
4392
4405
|
topLevel: AnthropicOutputEffort | undefined;
|
|
4393
4406
|
inserts: AnthropicControlInsert[];
|
|
@@ -4418,7 +4431,7 @@ function planAnthropicEffortControls(
|
|
|
4418
4431
|
}
|
|
4419
4432
|
tail = recorded ?? tail;
|
|
4420
4433
|
}
|
|
4421
|
-
if (current !== undefined && current !== tail) {
|
|
4434
|
+
if (ownChange && current !== undefined && current !== tail) {
|
|
4422
4435
|
inserts.push({
|
|
4423
4436
|
index: anthropicEffortInsertIndex(messages, messages.length, compactionReplay),
|
|
4424
4437
|
spec: { toolChanges: [], effort: current },
|
|
@@ -4428,6 +4441,32 @@ function planAnthropicEffortControls(
|
|
|
4428
4441
|
return { topLevel, inserts, record: { topLevel: topLevel ?? null, tail: tail ?? null } };
|
|
4429
4442
|
}
|
|
4430
4443
|
|
|
4444
|
+
/**
|
|
4445
|
+
* The effort whose thinking allowance an on-demand compaction request over
|
|
4446
|
+
* `messages` gets. It sends no effort change of its own, so once an earlier
|
|
4447
|
+
* request recorded an effort, the one it left in force applies, even when
|
|
4448
|
+
* `requested` differs or is off. When that request named none, the API's
|
|
4449
|
+
* per-model default applies, which is not known here: the request gets the
|
|
4450
|
+
* largest effort's allowance (`max_tokens` is a cap, not spend). Without a
|
|
4451
|
+
* recorded effort the request sets its own.
|
|
4452
|
+
*/
|
|
4453
|
+
export function resolveAnthropicCompactionEffort(
|
|
4454
|
+
model: Model<"anthropic-messages">,
|
|
4455
|
+
messages: readonly Message[],
|
|
4456
|
+
requested: Effort | undefined,
|
|
4457
|
+
): Effort | undefined {
|
|
4458
|
+
if (model.compat.supportsPerMessageEffort !== true) return requested;
|
|
4459
|
+
const records = collectAnthropicControlRecords(messages);
|
|
4460
|
+
if (!records.some(record => record.controls.effort)) return requested;
|
|
4461
|
+
const inForce = planAnthropicEffortControls(undefined, messages, records, true, undefined, false).record?.tail;
|
|
4462
|
+
if (!inForce) return requested && (model.thinking?.efforts.at(-1) ?? requested);
|
|
4463
|
+
if (requested && mapEffortToAnthropicAdaptiveEffort(model, requested) === inForce) return requested;
|
|
4464
|
+
return (
|
|
4465
|
+
model.thinking?.efforts?.findLast(effort => mapEffortToAnthropicAdaptiveEffort(model, effort) === inForce) ??
|
|
4466
|
+
requested
|
|
4467
|
+
);
|
|
4468
|
+
}
|
|
4469
|
+
|
|
4431
4470
|
/**
|
|
4432
4471
|
* Splice control markers into `messages`. Inserts sharing an index come from
|
|
4433
4472
|
* one request and merge into one control: tool changes first, then the effort.
|
|
@@ -4593,7 +4632,7 @@ function buildParams(
|
|
|
4593
4632
|
// Pre-compute system blocks so they occupy the right slot in the serialized body.
|
|
4594
4633
|
const shouldInjectClaudeCodeInstruction = isOAuthToken && model.compat.injectClaudeCodeInstruction !== false;
|
|
4595
4634
|
const firstUserMessageText = shouldInjectClaudeCodeInstruction
|
|
4596
|
-
?
|
|
4635
|
+
? extractClaudeCodeFirstWireUserMessageText(context.messages)
|
|
4597
4636
|
: "";
|
|
4598
4637
|
const systemBlocks = buildAnthropicSystemBlocks(context.systemPrompt, {
|
|
4599
4638
|
includeClaudeCodeInstruction: shouldInjectClaudeCodeInstruction,
|
|
@@ -4601,6 +4640,9 @@ function buildParams(
|
|
|
4601
4640
|
cacheControl,
|
|
4602
4641
|
});
|
|
4603
4642
|
|
|
4643
|
+
// An on-demand compaction request ends inside the conversation: it gets
|
|
4644
|
+
// nothing live requests lack there, so it makes no tool or effort change.
|
|
4645
|
+
const compactionRequest = compactionSupported ? options?.anthropicCompaction : undefined;
|
|
4604
4646
|
// Controls earlier requests recorded on their responses fix the declared
|
|
4605
4647
|
// tools, the top-level effort and every control message in between.
|
|
4606
4648
|
const records = collectAnthropicControlRecords(context.messages);
|
|
@@ -4608,6 +4650,7 @@ function buildParams(
|
|
|
4608
4650
|
context,
|
|
4609
4651
|
records,
|
|
4610
4652
|
model.compat.supportsMidConversationToolChanges === true,
|
|
4653
|
+
!compactionRequest,
|
|
4611
4654
|
);
|
|
4612
4655
|
|
|
4613
4656
|
// Pre-compute tools.
|
|
@@ -4750,7 +4793,6 @@ function buildParams(
|
|
|
4750
4793
|
// A new on-demand compaction request cannot carry context_management.
|
|
4751
4794
|
// Later turns carrying its signed block may keep clear_thinking as usual.
|
|
4752
4795
|
// Persisted encrypted threshold blocks alone need a legacy replay edit.
|
|
4753
|
-
const compactionRequest = compactionSupported ? options?.anthropicCompaction : undefined;
|
|
4754
4796
|
const signedReplay = compactionSupported && contextReplaysAnthropicCompaction(context.messages, model, "signed");
|
|
4755
4797
|
const legacyReplay =
|
|
4756
4798
|
compactionSupported && !signedReplay && contextReplaysAnthropicCompaction(context.messages, model, "legacy");
|
|
@@ -4776,6 +4818,7 @@ function buildParams(
|
|
|
4776
4818
|
records,
|
|
4777
4819
|
model.compat.supportsPerMessageEffort === true,
|
|
4778
4820
|
compactionReplay,
|
|
4821
|
+
!compactionRequest,
|
|
4779
4822
|
);
|
|
4780
4823
|
// `between_tools` returns a 400 at `xhigh`/`max` effort, and the effort in
|
|
4781
4824
|
// force from earlier turns outlives a thinking toggle. Fall back to the
|
|
@@ -4794,6 +4837,7 @@ function buildParams(
|
|
|
4794
4837
|
dropAllThinking: dropAllThinking || stripThinkingHistory,
|
|
4795
4838
|
droppedThinkingBlocks,
|
|
4796
4839
|
credentialId: options?.credentialId,
|
|
4840
|
+
compactionRequest,
|
|
4797
4841
|
},
|
|
4798
4842
|
);
|
|
4799
4843
|
// Anchor the stable tools+system head so it stays cached across turns; the
|
|
@@ -5053,6 +5097,8 @@ export function convertAnthropicMessages(
|
|
|
5053
5097
|
dropAllThinking?: boolean;
|
|
5054
5098
|
droppedThinkingBlocks?: ReadonlySet<string>;
|
|
5055
5099
|
credentialId?: number;
|
|
5100
|
+
/** On-demand compaction request: it ends inside the conversation, so it gets nothing live requests lack there. */
|
|
5101
|
+
compactionRequest?: AnthropicCompactionRequest;
|
|
5056
5102
|
},
|
|
5057
5103
|
): AnthropicMessageParam[] {
|
|
5058
5104
|
// Indices of params emitted from `developer` messages. After the main pass,
|
|
@@ -5064,21 +5110,33 @@ export function convertAnthropicMessages(
|
|
|
5064
5110
|
// and the developer upgrade below look through them: they were inserted
|
|
5065
5111
|
// by this provider, not authored in the conversation.
|
|
5066
5112
|
const controlParams = new Set<AnthropicMessageParam>();
|
|
5067
|
-
// Harness file metadata queued behind a replayed compaction block
|
|
5068
|
-
//
|
|
5069
|
-
//
|
|
5070
|
-
//
|
|
5071
|
-
// results
|
|
5072
|
-
|
|
5073
|
-
|
|
5074
|
-
|
|
5075
|
-
|
|
5076
|
-
|
|
5077
|
-
|
|
5078
|
-
|
|
5079
|
-
|
|
5080
|
-
|
|
5081
|
-
|
|
5113
|
+
// Harness file metadata queued behind a replayed compaction block, oldest
|
|
5114
|
+
// replay point first. Messages kept after a summary must reach the API
|
|
5115
|
+
// exactly as they were sent before, or their signed thinking no longer
|
|
5116
|
+
// matches its prefix; each entry therefore waits for the first message
|
|
5117
|
+
// created after its replay point (past any tool results that close an open
|
|
5118
|
+
// tool_use turn), or the end of the list.
|
|
5119
|
+
let pendingCompactionFiles: AnthropicCompactionFiles[] = [];
|
|
5120
|
+
const pushCompactionFiles = (filesText: string): void => {
|
|
5121
|
+
if (filesText.trim().length === 0) return;
|
|
5122
|
+
// The payload bypassed the `transformMessages` redaction pass, so
|
|
5123
|
+
// the metadata takes the same credential redaction here that the
|
|
5124
|
+
// dropped message text received there.
|
|
5125
|
+
params.push({ role: "user", content: redactSensitiveCredentials(filesText) });
|
|
5126
|
+
};
|
|
5127
|
+
const flushCompactionFiles = (before: number): void => {
|
|
5128
|
+
const due = pendingCompactionFiles.filter(files => files.after < before);
|
|
5129
|
+
pendingCompactionFiles = pendingCompactionFiles.filter(files => files.after >= before);
|
|
5130
|
+
for (const files of due) pushCompactionFiles(files.text);
|
|
5131
|
+
};
|
|
5132
|
+
// Summaries without `exactTail` replay their metadata after the first param
|
|
5133
|
+
// boundary past the block (clear of the fold and of an open tool_use turn):
|
|
5134
|
+
// thinking created after them was signed against that layout.
|
|
5135
|
+
let legacyCompactionFiles: string | undefined;
|
|
5136
|
+
const flushLegacyCompactionFiles = (): void => {
|
|
5137
|
+
if (legacyCompactionFiles === undefined) return;
|
|
5138
|
+
pushCompactionFiles(legacyCompactionFiles);
|
|
5139
|
+
legacyCompactionFiles = undefined;
|
|
5082
5140
|
};
|
|
5083
5141
|
|
|
5084
5142
|
const transformedMessages = transformMessages(
|
|
@@ -5118,17 +5176,23 @@ export function convertAnthropicMessages(
|
|
|
5118
5176
|
params.push(compactionParam);
|
|
5119
5177
|
// The block carries the verbatim API summary, so the message text
|
|
5120
5178
|
// (which holds the harness file lists) would be dropped with it.
|
|
5121
|
-
// Queue the file metadata
|
|
5122
|
-
//
|
|
5123
|
-
|
|
5124
|
-
if (
|
|
5125
|
-
pendingCompactionFiles
|
|
5179
|
+
// Queue the file metadata past the retained tail (see above); the
|
|
5180
|
+
// summary message carries the compaction's commit time.
|
|
5181
|
+
const { filesText, retainedFiles, exactTail } = msg.providerPayload;
|
|
5182
|
+
if (exactTail) {
|
|
5183
|
+
pendingCompactionFiles = [
|
|
5184
|
+
...(retainedFiles ?? []),
|
|
5185
|
+
...(filesText !== undefined ? [{ text: filesText, after: msg.timestamp }] : []),
|
|
5186
|
+
].sort((a, b) => a.after - b.after);
|
|
5187
|
+
} else {
|
|
5188
|
+
pendingCompactionFiles = [];
|
|
5189
|
+
legacyCompactionFiles = filesText;
|
|
5126
5190
|
}
|
|
5127
5191
|
continue;
|
|
5128
5192
|
}
|
|
5193
|
+
if (msg.role !== "toolResult") flushCompactionFiles(msg.timestamp);
|
|
5129
5194
|
if (msg.role === "user" || msg.role === "developer") {
|
|
5130
|
-
|
|
5131
|
-
flushCompactionFiles();
|
|
5195
|
+
flushLegacyCompactionFiles();
|
|
5132
5196
|
const payload =
|
|
5133
5197
|
msg.role === "developer" && msg.providerPayload?.type === "anthropicMessage"
|
|
5134
5198
|
? msg.providerPayload
|
|
@@ -5328,12 +5392,7 @@ export function convertAnthropicMessages(
|
|
|
5328
5392
|
};
|
|
5329
5393
|
copyPerCallContextMessage(assistantParam, msg);
|
|
5330
5394
|
params.push(assistantParam);
|
|
5331
|
-
|
|
5332
|
-
// their results must follow the turn contiguously, so the metadata
|
|
5333
|
-
// waits for the merged result message (or the end of the list).
|
|
5334
|
-
if (!blocks.some(block => block.type === "tool_use")) {
|
|
5335
|
-
flushCompactionFiles();
|
|
5336
|
-
}
|
|
5395
|
+
if (!blocks.some(block => block.type === "tool_use")) flushLegacyCompactionFiles();
|
|
5337
5396
|
} else if (msg.role === "toolResult") {
|
|
5338
5397
|
// Collect all consecutive toolResult messages, needed for z.ai Anthropic endpoint
|
|
5339
5398
|
const toolResults: ContentBlockParam[] = [];
|
|
@@ -5369,9 +5428,7 @@ export function convertAnthropicMessages(
|
|
|
5369
5428
|
|
|
5370
5429
|
// Add a single user message with all tool results
|
|
5371
5430
|
params.push(toolResultParam);
|
|
5372
|
-
|
|
5373
|
-
// metadata can follow without splitting the pairing.
|
|
5374
|
-
flushCompactionFiles();
|
|
5431
|
+
flushLegacyCompactionFiles();
|
|
5375
5432
|
}
|
|
5376
5433
|
}
|
|
5377
5434
|
|
|
@@ -5468,9 +5525,18 @@ export function convertAnthropicMessages(
|
|
|
5468
5525
|
params.splice(previous + 1, 0, { role: "user", content: "Continue." });
|
|
5469
5526
|
i = previous + 1;
|
|
5470
5527
|
}
|
|
5528
|
+
if (opts?.compactionRequest) {
|
|
5529
|
+
// Metadata due before `filesDueBefore` ends the summarized range; the
|
|
5530
|
+
// rest replays with the retained tail and is carried by the new summary
|
|
5531
|
+
// instead. Legacy metadata still pending here belongs after the first
|
|
5532
|
+
// retained turn, outside this request.
|
|
5533
|
+
flushCompactionFiles(opts.compactionRequest.filesDueBefore ?? Number.POSITIVE_INFINITY);
|
|
5534
|
+
return params;
|
|
5535
|
+
}
|
|
5471
5536
|
// A trailing compaction summary leaves its file metadata queued; emit it
|
|
5472
5537
|
// before the prefill check so the list ends the request as a user turn.
|
|
5473
|
-
flushCompactionFiles();
|
|
5538
|
+
flushCompactionFiles(Number.POSITIVE_INFINITY);
|
|
5539
|
+
flushLegacyCompactionFiles();
|
|
5474
5540
|
const last = skipControls(params.length, -1);
|
|
5475
5541
|
if (params[last]?.role === "assistant") {
|
|
5476
5542
|
params.splice(last + 1, 0, { role: "user", content: "Continue." });
|
package/src/providers/devin.ts
CHANGED
|
@@ -236,7 +236,9 @@ export const streamDevin: StreamFunction<"devin-agent"> = (
|
|
|
236
236
|
assignment = await assignDevinModel(model, turn, chatBaseUrl, fetchImpl, options?.signal);
|
|
237
237
|
output.upstreamModel = assignment.modelUid;
|
|
238
238
|
}
|
|
239
|
-
|
|
239
|
+
let request = buildDevinChatRequest(model, context, options, turn, assignment);
|
|
240
|
+
const replacementRequest = await options?.onPayload?.(request, model);
|
|
241
|
+
if (replacementRequest !== undefined) request = replacementRequest as typeof request;
|
|
240
242
|
const reqBytes = toBinary(GetChatMessageRequestSchema, request);
|
|
241
243
|
const gz = gzipSync(reqBytes);
|
|
242
244
|
logger.debug("devin: sending chat request", {
|
|
@@ -366,6 +366,7 @@ type ForwardedOptions = Pick<
|
|
|
366
366
|
| "streamIdleTimeoutMs"
|
|
367
367
|
| "streamFirstEventTimeoutMs"
|
|
368
368
|
| "maxTokens"
|
|
369
|
+
| "waitForTerminalDrain"
|
|
369
370
|
> & { apiKey: string; headers: Record<string, string> };
|
|
370
371
|
|
|
371
372
|
/** One resolved request attempt, shared by every wire encoder. */
|
|
@@ -580,6 +581,7 @@ export const streamFactoryDroid: StreamFunction<"factory-droid-agent"> = (
|
|
|
580
581
|
streamIdleTimeoutMs: options?.streamIdleTimeoutMs,
|
|
581
582
|
streamFirstEventTimeoutMs: options?.streamFirstEventTimeoutMs,
|
|
582
583
|
maxTokens: options?.maxTokens ?? scope.model.maxTokens ?? undefined,
|
|
584
|
+
waitForTerminalDrain: options?.waitForTerminalDrain,
|
|
583
585
|
headers: {
|
|
584
586
|
...buildIdentityHeaders({
|
|
585
587
|
upstream: scope.upstream,
|
|
@@ -250,6 +250,7 @@ export function streamGitLabDuo(
|
|
|
250
250
|
// like the azure-openai-responses mapping does.
|
|
251
251
|
disableReasoning: options.disableReasoning || options.forceReasoningOff,
|
|
252
252
|
toolChoice: options.toolChoice,
|
|
253
|
+
waitForTerminalDrain: options.waitForTerminalDrain,
|
|
253
254
|
} satisfies OpenAICompletionsOptions,
|
|
254
255
|
);
|
|
255
256
|
|
|
@@ -147,6 +147,7 @@ export function streamOpenAIAnthropicShim(
|
|
|
147
147
|
toolChoice: options?.toolChoice,
|
|
148
148
|
serviceTier: options?.serviceTier,
|
|
149
149
|
disableReasoning: options?.disableReasoning,
|
|
150
|
+
waitForTerminalDrain: options?.waitForTerminalDrain,
|
|
150
151
|
});
|
|
151
152
|
|
|
152
153
|
for await (const event of innerStream) {
|
|
@@ -46,6 +46,7 @@ import type {
|
|
|
46
46
|
Usage,
|
|
47
47
|
} from "../types";
|
|
48
48
|
import {
|
|
49
|
+
clampOpenAIResponsesImageDetailForReplay,
|
|
49
50
|
createOpenAIResponsesHistoryPayload,
|
|
50
51
|
dropMalformedOpenAIResponsesToolCalls,
|
|
51
52
|
getOpenAIResponsesHistoryItems,
|
|
@@ -273,18 +274,13 @@ const CODEX_WEBSOCKET_FIRST_EVENT_TIMEOUT_MS = Number($env.PI_CODEX_WEBSOCKET_FI
|
|
|
273
274
|
const CODEX_WEBSOCKET_RETRY_BUDGET = Number($env.PI_CODEX_WEBSOCKET_RETRY_BUDGET || CODEX_MAX_RETRIES);
|
|
274
275
|
const CODEX_WEBSOCKET_RETRY_DELAY_MS = Number($env.PI_CODEX_WEBSOCKET_RETRY_DELAY_MS || CODEX_RETRY_DELAY_MS);
|
|
275
276
|
const CODEX_WEBSOCKET_TRANSPORT_ERROR_PREFIX = "Codex websocket transport error";
|
|
276
|
-
/**
|
|
277
|
-
* The server's experimental native turn lane refuses `response.steer` with
|
|
278
|
-
* this code, then drops the in-flight response and closes the socket.
|
|
279
|
-
*/
|
|
280
|
-
const CODEX_NATIVE_LANE_STEER_REJECTED_CODE = "unsupported_native_inflight_message";
|
|
281
277
|
// The native-lane steering rejection is replayable: the session stops steering
|
|
282
278
|
// first, so the replay cannot trip it again.
|
|
283
279
|
const CODEX_RETRYABLE_EVENT_CODES = new Set([
|
|
284
280
|
"model_error",
|
|
285
281
|
"server_error",
|
|
286
282
|
"internal_error",
|
|
287
|
-
CODEX_NATIVE_LANE_STEER_REJECTED_CODE,
|
|
283
|
+
AIError.CODEX_NATIVE_LANE_STEER_REJECTED_CODE,
|
|
288
284
|
]);
|
|
289
285
|
const CODEX_RETRYABLE_EVENT_MESSAGE =
|
|
290
286
|
/processing your request|retry your request|temporar(?:y|ily)|overloaded|service.?unavailable|internal error|server error/i;
|
|
@@ -2709,6 +2705,7 @@ class CodexStreamProcessor {
|
|
|
2709
2705
|
const nativeOutputItems = runtime.finalizeNativeOutputItems();
|
|
2710
2706
|
const replayableResponseItems = sanitizeOpenAIResponsesAssistantHistoryItemsForReplay(
|
|
2711
2707
|
structuredCloneJSON(nativeOutputItems),
|
|
2708
|
+
{ supportsImageDetailOriginal: model.compat.supportsImageDetailOriginal },
|
|
2712
2709
|
);
|
|
2713
2710
|
if (responseId && replayableResponseItems && replayableResponseItems.length === nativeOutputItems.length) {
|
|
2714
2711
|
state.lastResponseId = responseId;
|
|
@@ -2749,7 +2746,7 @@ class CodexStreamProcessor {
|
|
|
2749
2746
|
if (
|
|
2750
2747
|
!state ||
|
|
2751
2748
|
!(error instanceof CodexProviderStreamError) ||
|
|
2752
|
-
error.code !== CODEX_NATIVE_LANE_STEER_REJECTED_CODE
|
|
2749
|
+
error.code !== AIError.CODEX_NATIVE_LANE_STEER_REJECTED_CODE
|
|
2753
2750
|
) {
|
|
2754
2751
|
return;
|
|
2755
2752
|
}
|
|
@@ -4083,7 +4080,7 @@ class CodexWebSocketConnection {
|
|
|
4083
4080
|
}
|
|
4084
4081
|
// The native lane answers `response.steer` with a plain `error` that
|
|
4085
4082
|
// also ends the response: refuse the submission, then fail the stream.
|
|
4086
|
-
if (parsed.type === "error" && parsed.code === CODEX_NATIVE_LANE_STEER_REJECTED_CODE) {
|
|
4083
|
+
if (parsed.type === "error" && parsed.code === AIError.CODEX_NATIVE_LANE_STEER_REJECTED_CODE) {
|
|
4087
4084
|
this.#refuseSteerWaiters(parsed.code, typeof parsed.message === "string" ? parsed.message : undefined);
|
|
4088
4085
|
}
|
|
4089
4086
|
this.#push(parsed);
|
|
@@ -4958,10 +4955,14 @@ function convertMessages(model: Model<"openai-codex-responses">, context: Contex
|
|
|
4958
4955
|
if (historyItems) {
|
|
4959
4956
|
const redactedHistoryItems = redactSensitiveInObject(historyItems).result as Array<ResponseInput[number]>;
|
|
4960
4957
|
const sanitizedHistoryItems = dropMalformedOpenAIResponsesToolCalls(redactedHistoryItems);
|
|
4958
|
+
const clampedHistoryItems = clampOpenAIResponsesImageDetailForReplay(
|
|
4959
|
+
sanitizedHistoryItems,
|
|
4960
|
+
model.compat.supportsImageDetailOriginal,
|
|
4961
|
+
);
|
|
4961
4962
|
const replayItems =
|
|
4962
4963
|
model.supportsComputerUse === true
|
|
4963
|
-
?
|
|
4964
|
-
: unrollCodexComputerItems(
|
|
4964
|
+
? clampedHistoryItems
|
|
4965
|
+
: unrollCodexComputerItems(clampedHistoryItems, model.compat.supportsImageDetailOriginal);
|
|
4965
4966
|
for (const item of replayItems) {
|
|
4966
4967
|
if (item.type === "custom_tool_call") {
|
|
4967
4968
|
customCallIds.add(item.call_id);
|
|
@@ -4995,7 +4996,9 @@ function convertMessages(model: Model<"openai-codex-responses">, context: Contex
|
|
|
4995
4996
|
const historyItems = providerPayload?.items as Array<Record<string, unknown>> | undefined;
|
|
4996
4997
|
let suppressHiddenEmptyFallback = false;
|
|
4997
4998
|
if (historyItems) {
|
|
4998
|
-
const sanitizedHistoryItems = sanitizeOpenAIResponsesAssistantHistoryItemsForReplay(historyItems
|
|
4999
|
+
const sanitizedHistoryItems = sanitizeOpenAIResponsesAssistantHistoryItemsForReplay(historyItems, {
|
|
5000
|
+
supportsImageDetailOriginal: model.compat.supportsImageDetailOriginal,
|
|
5001
|
+
});
|
|
4999
5002
|
if (sanitizedHistoryItems) {
|
|
5000
5003
|
const rawReplayItems =
|
|
5001
5004
|
model.supportsComputerUse === true
|
|
@@ -1336,6 +1336,7 @@ const streamOpenAICompletionsOnce = (
|
|
|
1336
1336
|
// and release the socket immediately (a queued `.return()` alone
|
|
1337
1337
|
// would wait on the never-arriving next chunk).
|
|
1338
1338
|
onGraceEnd: () => requestAbortController.abort(),
|
|
1339
|
+
awaitDrainOnReturn: options?.waitForTerminalDrain,
|
|
1339
1340
|
});
|
|
1340
1341
|
for await (const chunk of terminalAwareStream) {
|
|
1341
1342
|
if (!chunk || typeof chunk !== "object") continue;
|
|
@@ -1380,6 +1381,8 @@ const streamOpenAICompletionsOnce = (
|
|
|
1380
1381
|
// Trailing usage-only chunk (`stream_options.include_usage`) after
|
|
1381
1382
|
// `finish_reason`: the response is complete — stop pulling instead
|
|
1382
1383
|
// of waiting for `[DONE]`/close from hosts that never send either.
|
|
1384
|
+
// `iterateWithTerminalGrace` drains the tail in the background so
|
|
1385
|
+
// compliant hosts still get to relay `[DONE]` before the socket closes.
|
|
1383
1386
|
if (streamFinishedAt !== undefined && sawUsagePayload) break;
|
|
1384
1387
|
continue;
|
|
1385
1388
|
}
|