@oh-my-pi/pi-ai 16.3.6 → 16.3.7
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 +18 -0
- package/dist/types/api-registry.d.ts +1 -1
- package/dist/types/auth-broker/client.d.ts +2 -2
- package/dist/types/auth-broker/discover.d.ts +1 -1
- package/dist/types/auth-broker/index.d.ts +7 -7
- package/dist/types/auth-broker/refresher.d.ts +1 -1
- package/dist/types/auth-broker/remote-store.d.ts +6 -6
- package/dist/types/auth-broker/server.d.ts +1 -1
- package/dist/types/auth-broker/snapshot-cache.d.ts +1 -1
- package/dist/types/auth-broker/types.d.ts +2 -2
- package/dist/types/auth-gateway/index.d.ts +3 -3
- package/dist/types/auth-gateway/server.d.ts +3 -3
- package/dist/types/auth-gateway/types.d.ts +1 -1
- package/dist/types/auth-retry.d.ts +2 -2
- package/dist/types/auth-storage.d.ts +6 -6
- package/dist/types/dialect/anthropic.d.ts +1 -1
- package/dist/types/dialect/catalog.d.ts +1 -1
- package/dist/types/dialect/coercion.d.ts +1 -1
- package/dist/types/dialect/deepseek.d.ts +1 -1
- package/dist/types/dialect/demotion.d.ts +16 -14
- package/dist/types/dialect/examples.d.ts +1 -1
- package/dist/types/dialect/factory.d.ts +1 -1
- package/dist/types/dialect/gemini.d.ts +1 -1
- package/dist/types/dialect/gemma.d.ts +1 -1
- package/dist/types/dialect/glm.d.ts +1 -1
- package/dist/types/dialect/harmony.d.ts +1 -1
- package/dist/types/dialect/hermes.d.ts +1 -1
- package/dist/types/dialect/history.d.ts +2 -2
- package/dist/types/dialect/index.d.ts +11 -11
- package/dist/types/dialect/inventory.d.ts +1 -1
- package/dist/types/dialect/kimi.d.ts +1 -1
- package/dist/types/dialect/minimax.d.ts +1 -1
- package/dist/types/dialect/owned-stream.d.ts +2 -2
- package/dist/types/dialect/qwen3.d.ts +1 -1
- package/dist/types/dialect/rendering.d.ts +2 -2
- package/dist/types/dialect/thinking.d.ts +1 -1
- package/dist/types/dialect/types.d.ts +1 -1
- package/dist/types/dialect/xml.d.ts +1 -1
- package/dist/types/error/classes.d.ts +1 -1
- package/dist/types/error/finalize.d.ts +3 -3
- package/dist/types/error/flags.d.ts +1 -1
- package/dist/types/error/format.d.ts +1 -1
- package/dist/types/error/index.d.ts +13 -13
- package/dist/types/error/provider.d.ts +1 -1
- package/dist/types/error/rate-limit.d.ts +1 -1
- package/dist/types/index.d.ts +47 -47
- package/dist/types/provider-details.d.ts +1 -1
- package/dist/types/providers/amazon-bedrock.d.ts +1 -1
- package/dist/types/providers/anthropic-client.d.ts +3 -3
- package/dist/types/providers/anthropic-messages-server-schema.d.ts +1 -1
- package/dist/types/providers/anthropic-messages-server.d.ts +2 -2
- package/dist/types/providers/anthropic-wire.d.ts +1 -1
- package/dist/types/providers/anthropic.d.ts +3 -3
- package/dist/types/providers/aws-credentials.d.ts +2 -2
- package/dist/types/providers/azure-openai-responses.d.ts +2 -2
- package/dist/types/providers/cursor.d.ts +18 -3
- package/dist/types/providers/devin.d.ts +1 -1
- package/dist/types/providers/error-message.d.ts +1 -1
- package/dist/types/providers/github-copilot-headers.d.ts +1 -1
- package/dist/types/providers/gitlab-duo-workflow.d.ts +2 -2
- package/dist/types/providers/gitlab-duo.d.ts +2 -2
- package/dist/types/providers/google-auth.d.ts +1 -1
- package/dist/types/providers/google-gemini-cli.d.ts +6 -4
- package/dist/types/providers/google-interactions.d.ts +3 -3
- package/dist/types/providers/google-shared.d.ts +7 -5
- package/dist/types/providers/google-vertex.d.ts +2 -2
- package/dist/types/providers/google.d.ts +2 -2
- package/dist/types/providers/kimi.d.ts +3 -3
- package/dist/types/providers/mock.d.ts +2 -2
- package/dist/types/providers/ollama.d.ts +1 -1
- package/dist/types/providers/openai-anthropic-shim.d.ts +2 -2
- package/dist/types/providers/openai-chat-server-schema.d.ts +1 -1
- package/dist/types/providers/openai-chat-server.d.ts +2 -2
- package/dist/types/providers/openai-codex/request-transformer.d.ts +1 -1
- package/dist/types/providers/openai-codex/response-handler.d.ts +1 -1
- package/dist/types/providers/openai-codex-responses.d.ts +3 -3
- package/dist/types/providers/openai-completions.d.ts +3 -3
- package/dist/types/providers/openai-reasoning-fallback.d.ts +1 -1
- package/dist/types/providers/openai-responses-server-schema.d.ts +1 -1
- package/dist/types/providers/openai-responses-server.d.ts +2 -2
- package/dist/types/providers/openai-responses.d.ts +5 -5
- package/dist/types/providers/openai-shared.d.ts +10 -9
- package/dist/types/providers/pi-native-client.d.ts +1 -1
- package/dist/types/providers/pi-native-server.d.ts +2 -2
- package/dist/types/providers/register-builtins.d.ts +3 -3
- package/dist/types/providers/synthetic.d.ts +3 -3
- package/dist/types/providers/transform-messages.d.ts +1 -1
- package/dist/types/providers/vision-guard.d.ts +1 -1
- package/dist/types/registry/alibaba-coding-plan.d.ts +1 -1
- package/dist/types/registry/anthropic.d.ts +1 -1
- package/dist/types/registry/api-key-login.d.ts +1 -1
- package/dist/types/registry/api-key-validation.d.ts +1 -1
- package/dist/types/registry/baseten.d.ts +2 -2
- package/dist/types/registry/cerebras.d.ts +2 -2
- package/dist/types/registry/cloudflare-ai-gateway.d.ts +1 -1
- package/dist/types/registry/coreweave.d.ts +2 -2
- package/dist/types/registry/cursor.d.ts +1 -1
- package/dist/types/registry/deepseek.d.ts +1 -1
- package/dist/types/registry/devin.d.ts +1 -1
- package/dist/types/registry/firepass.d.ts +2 -2
- package/dist/types/registry/fireworks.d.ts +2 -2
- package/dist/types/registry/github-copilot.d.ts +1 -1
- package/dist/types/registry/gitlab-duo-workflow.d.ts +1 -1
- package/dist/types/registry/gitlab-duo.d.ts +1 -1
- package/dist/types/registry/google-antigravity.d.ts +1 -1
- package/dist/types/registry/google-gemini-cli.d.ts +1 -1
- package/dist/types/registry/huggingface.d.ts +2 -2
- package/dist/types/registry/index.d.ts +4 -4
- package/dist/types/registry/kagi.d.ts +1 -1
- package/dist/types/registry/kilo.d.ts +1 -1
- package/dist/types/registry/kimi-code.d.ts +1 -1
- package/dist/types/registry/litellm.d.ts +1 -1
- package/dist/types/registry/llama-cpp.d.ts +1 -1
- package/dist/types/registry/lm-studio.d.ts +1 -1
- package/dist/types/registry/minimax-code-cn.d.ts +1 -1
- package/dist/types/registry/minimax-code.d.ts +1 -1
- package/dist/types/registry/moonshot.d.ts +2 -2
- package/dist/types/registry/nanogpt.d.ts +2 -2
- package/dist/types/registry/nvidia.d.ts +1 -1
- package/dist/types/registry/oauth/anthropic.d.ts +3 -3
- package/dist/types/registry/oauth/callback-server.d.ts +1 -1
- package/dist/types/registry/oauth/cursor.d.ts +1 -1
- package/dist/types/registry/oauth/devin.d.ts +1 -1
- package/dist/types/registry/oauth/github-copilot.d.ts +2 -2
- package/dist/types/registry/oauth/gitlab-duo-workflow.d.ts +2 -2
- package/dist/types/registry/oauth/gitlab-duo.d.ts +1 -1
- package/dist/types/registry/oauth/google-antigravity.d.ts +1 -1
- package/dist/types/registry/oauth/google-gemini-cli.d.ts +1 -1
- package/dist/types/registry/oauth/google-oauth-shared.d.ts +2 -2
- package/dist/types/registry/oauth/index.d.ts +2 -2
- package/dist/types/registry/oauth/kimi.d.ts +1 -1
- package/dist/types/registry/oauth/minimax-code.d.ts +2 -2
- package/dist/types/registry/oauth/openai-codex.d.ts +1 -1
- package/dist/types/registry/oauth/opencode.d.ts +1 -1
- package/dist/types/registry/oauth/perplexity.d.ts +1 -1
- package/dist/types/registry/oauth/types.d.ts +2 -2
- package/dist/types/registry/oauth/wafer.d.ts +1 -1
- package/dist/types/registry/oauth/xai-oauth.d.ts +3 -3
- package/dist/types/registry/oauth/xiaomi.d.ts +1 -1
- package/dist/types/registry/ollama-cloud.d.ts +1 -1
- package/dist/types/registry/ollama.d.ts +1 -1
- package/dist/types/registry/openai-codex-device.d.ts +1 -1
- package/dist/types/registry/openai-codex.d.ts +1 -1
- package/dist/types/registry/opencode-go.d.ts +1 -1
- package/dist/types/registry/opencode-zen.d.ts +1 -1
- package/dist/types/registry/openrouter.d.ts +2 -2
- package/dist/types/registry/parallel.d.ts +1 -1
- package/dist/types/registry/perplexity.d.ts +2 -2
- package/dist/types/registry/qianfan.d.ts +2 -2
- package/dist/types/registry/qwen-portal.d.ts +1 -1
- package/dist/types/registry/registry.d.ts +69 -69
- package/dist/types/registry/sakana.d.ts +2 -2
- package/dist/types/registry/synthetic.d.ts +2 -2
- package/dist/types/registry/tavily.d.ts +1 -1
- package/dist/types/registry/together.d.ts +1 -1
- package/dist/types/registry/types.d.ts +1 -1
- package/dist/types/registry/umans.d.ts +2 -2
- package/dist/types/registry/venice.d.ts +2 -2
- package/dist/types/registry/vercel-ai-gateway.d.ts +1 -1
- package/dist/types/registry/vllm.d.ts +1 -1
- package/dist/types/registry/wafer-serverless.d.ts +1 -1
- package/dist/types/registry/xai-oauth.d.ts +1 -1
- package/dist/types/registry/xiaomi-token-plan-ams.d.ts +1 -1
- package/dist/types/registry/xiaomi-token-plan-cn.d.ts +1 -1
- package/dist/types/registry/xiaomi-token-plan-sgp.d.ts +1 -1
- package/dist/types/registry/xiaomi.d.ts +1 -1
- package/dist/types/registry/zai.d.ts +2 -2
- package/dist/types/registry/zenmux.d.ts +2 -2
- package/dist/types/registry/zhipu-coding-plan.d.ts +2 -2
- package/dist/types/stream.d.ts +6 -6
- package/dist/types/types.d.ts +24 -21
- package/dist/types/usage/claude.d.ts +1 -1
- package/dist/types/usage/gemini.d.ts +1 -1
- package/dist/types/usage/github-copilot.d.ts +1 -1
- package/dist/types/usage/google-antigravity.d.ts +1 -1
- package/dist/types/usage/kimi.d.ts +1 -1
- package/dist/types/usage/minimax-code.d.ts +1 -1
- package/dist/types/usage/ollama.d.ts +1 -1
- package/dist/types/usage/openai-codex-reset.d.ts +1 -1
- package/dist/types/usage/openai-codex.d.ts +1 -1
- package/dist/types/usage/opencode-go.d.ts +1 -1
- package/dist/types/usage/zai.d.ts +1 -1
- package/dist/types/usage.d.ts +1 -1
- package/dist/types/utils/block-symbols.d.ts +15 -0
- package/dist/types/utils/empty-completion-retry.d.ts +2 -2
- package/dist/types/utils/event-stream.d.ts +1 -1
- package/dist/types/utils/harmony-leak.d.ts +1 -1
- package/dist/types/utils/http-inspector.d.ts +19 -0
- package/dist/types/utils/leaked-thinking-stream.d.ts +1 -1
- package/dist/types/utils/openai-http.d.ts +3 -9
- package/dist/types/utils/provider-response.d.ts +1 -1
- package/dist/types/utils/proxy.d.ts +1 -1
- package/dist/types/utils/request-debug.d.ts +1 -1
- package/dist/types/utils/retry.d.ts +1 -1
- package/dist/types/utils/schema/equality.d.ts +1 -1
- package/dist/types/utils/schema/index.d.ts +15 -15
- package/dist/types/utils/schema/normalize.d.ts +2 -2
- package/dist/types/utils/schema/spill.d.ts +1 -1
- package/dist/types/utils/schema/wire.d.ts +1 -1
- package/dist/types/utils/sse-debug.d.ts +1 -1
- package/dist/types/utils/thinking-loop.d.ts +2 -2
- package/dist/types/utils/tool-call-loop-guard.d.ts +1 -1
- package/dist/types/utils/tool-choice.d.ts +1 -1
- package/dist/types/utils/validation.d.ts +1 -1
- package/dist/types/utils.d.ts +14 -2
- package/package.json +4 -4
- package/src/dialect/demotion.ts +18 -19
- package/src/error/rate-limit.ts +1 -1
- package/src/providers/azure-openai-responses.ts +3 -2
- package/src/providers/cursor.ts +36 -0
- package/src/providers/google-gemini-cli.ts +12 -3
- package/src/providers/google-shared.ts +3 -1
- package/src/providers/mock.ts +10 -3
- package/src/providers/openai-codex-responses.ts +47 -39
- package/src/providers/openai-completions.ts +48 -9
- package/src/providers/openai-responses.ts +37 -23
- package/src/providers/openai-shared.ts +145 -26
- package/src/providers/transform-messages.ts +63 -9
- package/src/registry/oauth/wafer.ts +1 -1
- package/src/stream.ts +6 -0
- package/src/types.ts +27 -3
- package/src/utils/block-symbols.ts +18 -0
- package/src/utils/http-inspector.ts +32 -4
- package/src/utils/openai-http.ts +6 -9
- package/src/utils/retry-after.ts +12 -1
- package/src/utils.ts +78 -0
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
import { parseGitHubCopilotApiKey } from "@oh-my-pi/pi-catalog/wire/github-copilot";
|
|
23
23
|
import {
|
|
24
24
|
$env,
|
|
25
|
+
classifyJsonPrefix,
|
|
25
26
|
extractHttpStatusFromError,
|
|
26
27
|
logger,
|
|
27
28
|
parseStreamingJson,
|
|
@@ -39,7 +40,6 @@ import {
|
|
|
39
40
|
type MessageAttribution,
|
|
40
41
|
type Model,
|
|
41
42
|
OPENAI_MAX_OUTPUT_TOKENS,
|
|
42
|
-
type Provider,
|
|
43
43
|
type ServiceTier,
|
|
44
44
|
type StopReason,
|
|
45
45
|
type StreamOptions,
|
|
@@ -50,6 +50,7 @@ import {
|
|
|
50
50
|
type Tool,
|
|
51
51
|
type ToolCall,
|
|
52
52
|
type ToolResultMessage,
|
|
53
|
+
type Usage,
|
|
53
54
|
} from "../types";
|
|
54
55
|
import {
|
|
55
56
|
getOpenAIResponsesHistoryItems,
|
|
@@ -57,6 +58,8 @@ import {
|
|
|
57
58
|
normalizeResponsesToolCallId,
|
|
58
59
|
normalizeSystemPrompts,
|
|
59
60
|
resolveCacheRetention,
|
|
61
|
+
sanitizeOpenAIResponsesAssistantFallbackItemsForReplay,
|
|
62
|
+
sanitizeOpenAIResponsesAssistantHistoryItemsForReplay,
|
|
60
63
|
sanitizeOpenAIResponsesHistoryItemsForReplay,
|
|
61
64
|
} from "../utils";
|
|
62
65
|
import {
|
|
@@ -271,9 +274,9 @@ export function resolveOpenAIRequestSetup(
|
|
|
271
274
|
export function applyOpenAIServiceTier(
|
|
272
275
|
params: { service_tier?: ServiceTier | null | undefined },
|
|
273
276
|
serviceTier: ServiceTier | null | undefined,
|
|
274
|
-
|
|
277
|
+
model: Pick<Model, "provider" | "api" | "id">,
|
|
275
278
|
): void {
|
|
276
|
-
if (!shouldSendServiceTier(serviceTier,
|
|
279
|
+
if (!shouldSendServiceTier(serviceTier, model)) return;
|
|
277
280
|
if (serviceTier === "flex" || serviceTier === "scale" || serviceTier === "priority") {
|
|
278
281
|
params.service_tier = serviceTier;
|
|
279
282
|
}
|
|
@@ -341,6 +344,7 @@ export interface OpenAIUsageAccounting {
|
|
|
341
344
|
cacheWrite: number;
|
|
342
345
|
totalTokens: number;
|
|
343
346
|
reasoningTokens?: number;
|
|
347
|
+
orchestration?: Usage["orchestration"];
|
|
344
348
|
}
|
|
345
349
|
|
|
346
350
|
export function calculateOpenAIUsageAccounting(accounting: OpenAIUsageAccountingInput): OpenAIUsageAccounting {
|
|
@@ -1405,39 +1409,51 @@ export function buildResponsesInput<TApi extends Api>(options: BuildResponsesInp
|
|
|
1405
1409
|
});
|
|
1406
1410
|
} else if (msg.role === "assistant") {
|
|
1407
1411
|
const assistantMsg = msg as AssistantMessage;
|
|
1412
|
+
// Providers replay stale native items even when the current request has
|
|
1413
|
+
// disabled native replay (cold session state, filter policy). Consult
|
|
1414
|
+
// the payload sanitizer directly so hidden-empty turns are recognized
|
|
1415
|
+
// on both the warm and cold paths.
|
|
1408
1416
|
const providerPayload =
|
|
1409
|
-
options.
|
|
1410
|
-
assistantMsg.api === options.model.api &&
|
|
1411
|
-
assistantMsg.model === options.model.id
|
|
1417
|
+
assistantMsg.api === options.model.api && assistantMsg.model === options.model.id
|
|
1412
1418
|
? getOpenAIResponsesHistoryPayload(
|
|
1413
1419
|
assistantMsg.providerPayload,
|
|
1414
1420
|
options.model.provider,
|
|
1415
1421
|
assistantMsg.provider,
|
|
1416
1422
|
)
|
|
1417
1423
|
: undefined;
|
|
1424
|
+
const nativeReplayEnabled = options.nativeHistory?.replay === true;
|
|
1418
1425
|
const historyItems = providerPayload?.items;
|
|
1426
|
+
let suppressHiddenEmptyFallback = false;
|
|
1419
1427
|
if (historyItems) {
|
|
1420
|
-
const sanitizedHistoryItems =
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1428
|
+
const sanitizedHistoryItems = sanitizeOpenAIResponsesAssistantHistoryItemsForReplay(
|
|
1429
|
+
filterReasoning(historyItems),
|
|
1430
|
+
);
|
|
1431
|
+
if (nativeReplayEnabled && sanitizedHistoryItems) {
|
|
1432
|
+
if (providerPayload?.dt) {
|
|
1433
|
+
messages.push(...sanitizedHistoryItems);
|
|
1434
|
+
} else {
|
|
1435
|
+
messages.splice(0, messages.length, ...sanitizedHistoryItems);
|
|
1436
|
+
}
|
|
1437
|
+
knownCallIds = collectKnownCallIds(messages);
|
|
1438
|
+
for (const id of collectCustomCallIds(messages)) customCallIds.add(id);
|
|
1439
|
+
msgIndex++;
|
|
1440
|
+
continue;
|
|
1425
1441
|
}
|
|
1426
|
-
|
|
1427
|
-
for (const id of collectCustomCallIds(messages)) customCallIds.add(id);
|
|
1428
|
-
msgIndex++;
|
|
1429
|
-
continue;
|
|
1442
|
+
if (!sanitizedHistoryItems) suppressHiddenEmptyFallback = true;
|
|
1430
1443
|
}
|
|
1431
1444
|
|
|
1432
|
-
const
|
|
1445
|
+
const convertedOutputItems = convertResponsesAssistantMessage(
|
|
1433
1446
|
assistantMsg,
|
|
1434
1447
|
options.model,
|
|
1435
1448
|
msgIndex,
|
|
1436
1449
|
knownCallIds,
|
|
1437
|
-
includeThinkingSignatures,
|
|
1450
|
+
suppressHiddenEmptyFallback ? false : includeThinkingSignatures,
|
|
1438
1451
|
customCallIds,
|
|
1439
1452
|
options.preserveAssistantMessageIds,
|
|
1440
1453
|
);
|
|
1454
|
+
const outputItems = suppressHiddenEmptyFallback
|
|
1455
|
+
? sanitizeOpenAIResponsesAssistantFallbackItemsForReplay(convertedOutputItems)
|
|
1456
|
+
: convertedOutputItems;
|
|
1441
1457
|
if (outputItems.length === 0) continue;
|
|
1442
1458
|
messages.push(...outputItems);
|
|
1443
1459
|
} else if (msg.role === "toolResult") {
|
|
@@ -1874,6 +1890,58 @@ export async function processResponsesStream<TApi extends Api>(
|
|
|
1874
1890
|
};
|
|
1875
1891
|
const hasOpenItemKey = (event: { output_index?: number; item_id?: string }): boolean =>
|
|
1876
1892
|
typeof event.output_index === "number" || event.item_id !== undefined;
|
|
1893
|
+
const startsJsonObjectDelta = (delta: unknown): boolean => {
|
|
1894
|
+
if (typeof delta !== "string") return false;
|
|
1895
|
+
for (let index = 0; index < delta.length; index++) {
|
|
1896
|
+
const code = delta.charCodeAt(index);
|
|
1897
|
+
if (code === 0x09 || code === 0x0a || code === 0x0d || code === 0x20) continue;
|
|
1898
|
+
return code === 0x7b;
|
|
1899
|
+
}
|
|
1900
|
+
return false;
|
|
1901
|
+
};
|
|
1902
|
+
const shouldAdvanceIdentifierlessFunctionDelta = (
|
|
1903
|
+
event: { output_index?: number; item_id?: string; delta?: unknown },
|
|
1904
|
+
candidate: StreamingItem,
|
|
1905
|
+
): boolean => {
|
|
1906
|
+
const delta = event.delta;
|
|
1907
|
+
if (
|
|
1908
|
+
hasOpenItemKey(event) ||
|
|
1909
|
+
typeof delta !== "string" ||
|
|
1910
|
+
!startsJsonObjectDelta(delta) ||
|
|
1911
|
+
candidate.item.type !== "function_call" ||
|
|
1912
|
+
candidate.block.type !== "toolCall"
|
|
1913
|
+
) {
|
|
1914
|
+
return false;
|
|
1915
|
+
}
|
|
1916
|
+
const partial = candidate.block[kStreamingPartialJson];
|
|
1917
|
+
if (partial.trim().length === 0) return false;
|
|
1918
|
+
// A `{`-starting identifierless delta is ambiguous: the opening of a new
|
|
1919
|
+
// sibling call, or continuation bytes inside the candidate's own argument
|
|
1920
|
+
// JSON (`{"command":"echo ` + `{1..3}"}`). Advance only when the candidate
|
|
1921
|
+
// cannot absorb the delta: its buffer is already one complete JSON value,
|
|
1922
|
+
// already unsalvageable (lossy hosts abandon buffers mid-string, leaving
|
|
1923
|
+
// raw control characters strict JSON forbids), or the concatenation would
|
|
1924
|
+
// break it. Otherwise the delta is a legal continuation and must stay.
|
|
1925
|
+
const state = classifyJsonPrefix(partial);
|
|
1926
|
+
if (state !== "prefix") return true;
|
|
1927
|
+
return classifyJsonPrefix(partial + delta) === "invalid";
|
|
1928
|
+
};
|
|
1929
|
+
const hasLaterUnfinishedFunctionCall = (start: number): boolean => {
|
|
1930
|
+
for (let index = start + 1; index < openItemsInOrder.length; index++) {
|
|
1931
|
+
const candidate = openItemsInOrder[index];
|
|
1932
|
+
if (
|
|
1933
|
+
candidate?.item.type === "function_call" &&
|
|
1934
|
+
candidate.block.type === "toolCall" &&
|
|
1935
|
+
!candidate.block[kStreamingArgumentsDone]
|
|
1936
|
+
) {
|
|
1937
|
+
return true;
|
|
1938
|
+
}
|
|
1939
|
+
}
|
|
1940
|
+
return false;
|
|
1941
|
+
};
|
|
1942
|
+
|
|
1943
|
+
let identifierlessFunctionDeltaTarget: StreamingItem | undefined;
|
|
1944
|
+
|
|
1877
1945
|
const lookupOpenToolCallAlias = (
|
|
1878
1946
|
event: { output_index?: number; item_id?: string },
|
|
1879
1947
|
type: "function_call" | "custom_tool_call",
|
|
@@ -1902,17 +1970,42 @@ export async function processResponsesStream<TApi extends Api>(
|
|
|
1902
1970
|
const lookupOpenFunctionCallItem = (event: {
|
|
1903
1971
|
output_index?: number;
|
|
1904
1972
|
item_id?: string;
|
|
1973
|
+
delta?: unknown;
|
|
1905
1974
|
}): StreamingItem | undefined => {
|
|
1906
1975
|
if (hasOpenItemKey(event)) return lookupOpenToolCallAlias(event, "function_call");
|
|
1907
|
-
|
|
1976
|
+
const canContinuePreviousIdentifierlessDelta = typeof event.delta === "string";
|
|
1977
|
+
if (canContinuePreviousIdentifierlessDelta && identifierlessFunctionDeltaTarget) {
|
|
1978
|
+
const targetIndex = openItemsInOrder.indexOf(identifierlessFunctionDeltaTarget);
|
|
1979
|
+
const target = targetIndex >= 0 ? openItemsInOrder[targetIndex] : undefined;
|
|
1980
|
+
if (
|
|
1981
|
+
target?.item.type === "function_call" &&
|
|
1982
|
+
target.block.type === "toolCall" &&
|
|
1983
|
+
!target.block[kStreamingArgumentsDone]
|
|
1984
|
+
) {
|
|
1985
|
+
const shouldAdvanceFromTarget =
|
|
1986
|
+
shouldAdvanceIdentifierlessFunctionDelta(event, target) && hasLaterUnfinishedFunctionCall(targetIndex);
|
|
1987
|
+
if (!shouldAdvanceFromTarget) return target;
|
|
1988
|
+
} else {
|
|
1989
|
+
identifierlessFunctionDeltaTarget = undefined;
|
|
1990
|
+
}
|
|
1991
|
+
}
|
|
1992
|
+
let skippedStartedCandidate = false;
|
|
1993
|
+
for (let index = 0; index < openItemsInOrder.length; index++) {
|
|
1994
|
+
const candidate = openItemsInOrder[index]!;
|
|
1908
1995
|
if (
|
|
1909
1996
|
candidate.item.type === "function_call" &&
|
|
1910
1997
|
candidate.block.type === "toolCall" &&
|
|
1911
1998
|
!candidate.block[kStreamingArgumentsDone]
|
|
1912
1999
|
) {
|
|
2000
|
+
if (shouldAdvanceIdentifierlessFunctionDelta(event, candidate) && hasLaterUnfinishedFunctionCall(index)) {
|
|
2001
|
+
skippedStartedCandidate = true;
|
|
2002
|
+
continue;
|
|
2003
|
+
}
|
|
2004
|
+
if (canContinuePreviousIdentifierlessDelta) identifierlessFunctionDeltaTarget = candidate;
|
|
1913
2005
|
return candidate;
|
|
1914
2006
|
}
|
|
1915
2007
|
}
|
|
2008
|
+
if (skippedStartedCandidate && startsJsonObjectDelta(event.delta)) return undefined;
|
|
1916
2009
|
return lastOpenItem?.item.type === "function_call" ? lastOpenItem : undefined;
|
|
1917
2010
|
};
|
|
1918
2011
|
const closeOpenItem = (
|
|
@@ -1937,6 +2030,7 @@ export async function processResponsesStream<TApi extends Api>(
|
|
|
1937
2030
|
const index = openItemsInOrder.indexOf(entry);
|
|
1938
2031
|
if (index >= 0) openItemsInOrder.splice(index, 1);
|
|
1939
2032
|
}
|
|
2033
|
+
if (entry && identifierlessFunctionDeltaTarget === entry) identifierlessFunctionDeltaTarget = undefined;
|
|
1940
2034
|
if (entry && lastOpenItem === entry) lastOpenItem = null;
|
|
1941
2035
|
};
|
|
1942
2036
|
const contentIndexOf = (block: ThinkingContent | TextContent | StreamingToolCallBlock): number =>
|
|
@@ -2150,9 +2244,11 @@ export async function processResponsesStream<TApi extends Api>(
|
|
|
2150
2244
|
const block = entry?.block.type === "toolCall" ? entry.block : undefined;
|
|
2151
2245
|
const args = block?.[kStreamingArgumentsDone]
|
|
2152
2246
|
? block.arguments
|
|
2153
|
-
:
|
|
2154
|
-
? parseStreamingJson(
|
|
2155
|
-
:
|
|
2247
|
+
: item.arguments
|
|
2248
|
+
? parseStreamingJson(item.arguments)
|
|
2249
|
+
: block?.[kStreamingPartialJson]
|
|
2250
|
+
? parseStreamingJson(block[kStreamingPartialJson])
|
|
2251
|
+
: parseStreamingJson("{}");
|
|
2156
2252
|
const toolCall: ToolCall = {
|
|
2157
2253
|
type: "toolCall",
|
|
2158
2254
|
id: encodeResponsesToolCallId(item.call_id, item.id),
|
|
@@ -2385,7 +2481,7 @@ type CommonSamplingOptions = Pick<
|
|
|
2385
2481
|
export function applyCommonResponsesSamplingParams<P extends CommonResponsesParams>(
|
|
2386
2482
|
params: P,
|
|
2387
2483
|
options: CommonSamplingOptions | undefined,
|
|
2388
|
-
model: Pick<Model, "provider" | "omitMaxOutputTokens" | "maxTokens">,
|
|
2484
|
+
model: Pick<Model, "provider" | "api" | "id" | "omitMaxOutputTokens" | "maxTokens">,
|
|
2389
2485
|
): void {
|
|
2390
2486
|
if (options?.maxTokens && !model.omitMaxOutputTokens) {
|
|
2391
2487
|
params.max_output_tokens = Math.min(
|
|
@@ -2400,7 +2496,7 @@ export function applyCommonResponsesSamplingParams<P extends CommonResponsesPara
|
|
|
2400
2496
|
if (options?.minP !== undefined) params.min_p = options.minP;
|
|
2401
2497
|
if (options?.presencePenalty !== undefined) params.presence_penalty = options.presencePenalty;
|
|
2402
2498
|
if (options?.repetitionPenalty !== undefined) params.repetition_penalty = options.repetitionPenalty;
|
|
2403
|
-
applyOpenAIServiceTier(params, options?.serviceTier, model
|
|
2499
|
+
applyOpenAIServiceTier(params, options?.serviceTier, model);
|
|
2404
2500
|
}
|
|
2405
2501
|
|
|
2406
2502
|
type ReasoningOptions = {
|
|
@@ -2522,19 +2618,42 @@ export function populateResponsesUsageFromResponse(
|
|
|
2522
2618
|
if (!usage) return;
|
|
2523
2619
|
const details = usage.input_tokens_details;
|
|
2524
2620
|
const outputDetails = usage.output_tokens_details;
|
|
2621
|
+
const reportedInputTokens = usage.input_tokens ?? 0;
|
|
2622
|
+
const reportedOutputTokens = usage.output_tokens ?? 0;
|
|
2623
|
+
const reportedCachedTokens = details?.cached_tokens ?? usage.prompt_cache_hit_tokens ?? 0;
|
|
2525
2624
|
const orchestrationInputTokens = details?.orchestration_input_tokens ?? 0;
|
|
2526
2625
|
const orchestrationInputCachedTokens = details?.orchestration_input_cached_tokens ?? 0;
|
|
2527
2626
|
const orchestrationOutputTokens = outputDetails?.orchestration_output_tokens ?? 0;
|
|
2627
|
+
const reportedTotalTokens = typeof usage.total_tokens === "number" ? usage.total_tokens : undefined;
|
|
2628
|
+
const reportedPrimaryTokens = reportedInputTokens + reportedOutputTokens;
|
|
2629
|
+
const reportedWithSeparateOrchestration =
|
|
2630
|
+
reportedPrimaryTokens + orchestrationInputTokens + orchestrationOutputTokens;
|
|
2631
|
+
const primaryIncludesOrchestration =
|
|
2632
|
+
reportedTotalTokens !== undefined &&
|
|
2633
|
+
orchestrationInputTokens + orchestrationOutputTokens > 0 &&
|
|
2634
|
+
Math.abs(reportedTotalTokens - reportedPrimaryTokens) <=
|
|
2635
|
+
Math.abs(reportedTotalTokens - reportedWithSeparateOrchestration);
|
|
2636
|
+
const orchestrationInputCached = Math.min(orchestrationInputTokens, orchestrationInputCachedTokens);
|
|
2637
|
+
const orchestrationInput = Math.max(0, orchestrationInputTokens - orchestrationInputCached);
|
|
2528
2638
|
const accounting = calculateOpenAIUsageAccounting({
|
|
2529
|
-
promptTokens: (
|
|
2530
|
-
outputTokens: (
|
|
2531
|
-
cachedTokens: (
|
|
2639
|
+
promptTokens: Math.max(0, reportedInputTokens - (primaryIncludesOrchestration ? orchestrationInputTokens : 0)),
|
|
2640
|
+
outputTokens: Math.max(0, reportedOutputTokens - (primaryIncludesOrchestration ? orchestrationOutputTokens : 0)),
|
|
2641
|
+
cachedTokens: Math.max(0, reportedCachedTokens - (primaryIncludesOrchestration ? orchestrationInputCached : 0)),
|
|
2532
2642
|
reasoningTokens: outputDetails?.reasoning_tokens ?? 0,
|
|
2533
2643
|
cacheWriteOpenRouter: details?.cache_write_tokens ?? undefined,
|
|
2534
2644
|
cacheWriteDeepSeek: usage.prompt_cache_miss_tokens ?? undefined,
|
|
2535
2645
|
hasDeepSeekCacheHitAndMiss:
|
|
2536
2646
|
usage.prompt_cache_hit_tokens !== undefined && usage.prompt_cache_miss_tokens !== undefined,
|
|
2537
2647
|
});
|
|
2648
|
+
const orchestrationTotal = orchestrationInput + orchestrationInputCached + orchestrationOutputTokens;
|
|
2649
|
+
if (orchestrationTotal > 0) {
|
|
2650
|
+
accounting.orchestration = {
|
|
2651
|
+
...(orchestrationInput > 0 ? { input: orchestrationInput } : {}),
|
|
2652
|
+
...(orchestrationInputCached > 0 ? { cacheRead: orchestrationInputCached } : {}),
|
|
2653
|
+
...(orchestrationOutputTokens > 0 ? { output: orchestrationOutputTokens } : {}),
|
|
2654
|
+
};
|
|
2655
|
+
accounting.totalTokens = reportedTotalTokens ?? accounting.totalTokens + orchestrationTotal;
|
|
2656
|
+
}
|
|
2538
2657
|
|
|
2539
2658
|
// Wholesale replacement must not drop provider-annotated extras (Copilot
|
|
2540
2659
|
// premium-request accounting): the failed/cancelled paths throw right after
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { renderDemotedThinking } from "../dialect/demotion";
|
|
2
2
|
import type { Api, AssistantMessage, Message, Model, ToolCall, ToolResultMessage, UserMessage } from "../types";
|
|
3
|
+
import { isDemotedThinking, kDemotedThinking } from "../utils/block-symbols";
|
|
3
4
|
|
|
4
5
|
const enum ToolCallStatus {
|
|
5
6
|
/** A tool result has already been emitted for this tool call; later duplicates must be skipped. */
|
|
@@ -402,6 +403,31 @@ export function transformMessages<TApi extends Api>(
|
|
|
402
403
|
assistantMsg.content.some(b => b.type === "toolCall");
|
|
403
404
|
const lastBlockIndex = assistantMsg.content.length - 1;
|
|
404
405
|
|
|
406
|
+
const anthropicVisibleThinkingSurvivesReplay = (
|
|
407
|
+
candidate: AssistantMessage["content"][number],
|
|
408
|
+
candidateIndex: number,
|
|
409
|
+
): boolean => {
|
|
410
|
+
if (candidate.type !== "thinking") return false;
|
|
411
|
+
if (!isAnthropicReplay) return false;
|
|
412
|
+
if (isLatestSurvivingAssistant && abandonedToolUse) return true;
|
|
413
|
+
const candidateSignatureUntrustworthy =
|
|
414
|
+
abandonedToolUse || (invalidStopReason && candidateIndex === lastBlockIndex);
|
|
415
|
+
const replaySignature =
|
|
416
|
+
candidateSignatureUntrustworthy && candidate.thinkingSignature ? undefined : candidate.thinkingSignature;
|
|
417
|
+
if (!replaySignature && (!candidate.thinking || candidate.thinking.trim() === "")) return false;
|
|
418
|
+
if (isSameModel && isSigningAnthropicTarget && (!replaySignature || replaySignature.trim() === "")) {
|
|
419
|
+
return false;
|
|
420
|
+
}
|
|
421
|
+
return true;
|
|
422
|
+
};
|
|
423
|
+
const hasVisibleAnthropicThinking = assistantMsg.content.some(candidate => candidate.type === "thinking");
|
|
424
|
+
const dropsAllSameModelVisibleThinking =
|
|
425
|
+
isAnthropicReplay &&
|
|
426
|
+
isSameModel &&
|
|
427
|
+
isSigningAnthropicTarget &&
|
|
428
|
+
hasVisibleAnthropicThinking &&
|
|
429
|
+
!assistantMsg.content.some(anthropicVisibleThinkingSurvivesReplay);
|
|
430
|
+
|
|
405
431
|
const transformedContent = assistantMsg.content.flatMap((block, blockIndex) => {
|
|
406
432
|
if (block.type === "thinking") {
|
|
407
433
|
// Only an aborted/errored turn's final (mid-stream) block can hold a
|
|
@@ -437,13 +463,15 @@ export function transformMessages<TApi extends Api>(
|
|
|
437
463
|
return [];
|
|
438
464
|
}
|
|
439
465
|
// Same-model Anthropic replay to a signature-enforcing endpoint
|
|
440
|
-
//
|
|
441
|
-
//
|
|
442
|
-
//
|
|
443
|
-
//
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
466
|
+
// requires valid signatures to natively replay thinking blocks.
|
|
467
|
+
// Both undefined and empty string signatures are invalid and must
|
|
468
|
+
// be dropped entirely — not demoted to text. Demotion would cause
|
|
469
|
+
// the reasoning_extraction safety classifier to refuse the response.
|
|
470
|
+
if (
|
|
471
|
+
isSameModel &&
|
|
472
|
+
isSigningAnthropicTarget &&
|
|
473
|
+
(!sanitized.thinkingSignature || sanitized.thinkingSignature.trim() === "")
|
|
474
|
+
) {
|
|
447
475
|
return [];
|
|
448
476
|
}
|
|
449
477
|
return sanitized;
|
|
@@ -476,9 +504,21 @@ export function transformMessages<TApi extends Api>(
|
|
|
476
504
|
// TARGET model's own canonical thinking-block dialect (e.g. a ```thinking
|
|
477
505
|
// fence for Gemini) so it reads as reasoning rather than bare prose the
|
|
478
506
|
// model might mimic.
|
|
507
|
+
// Mark the demoted block (symbol-keyed, never serialized) instead of
|
|
508
|
+
// baking a separator into its text: the openai-completions flatten —
|
|
509
|
+
// the one consumer that joins adjacent text blocks into a single
|
|
510
|
+
// string — inserts a paragraph break after marked blocks, so the
|
|
511
|
+
// bare Anthropic-dialect output (or any dialect's wrapped output
|
|
512
|
+
// whose closing tag isn't a natural word boundary) can't glue onto
|
|
513
|
+
// the following visible-text block, while ordinary adjacent text
|
|
514
|
+
// blocks stitched from streaming / bridges / imported transcripts
|
|
515
|
+
// stay byte-identical. A separator baked into the block text would
|
|
516
|
+
// leak to non-flattening targets: Anthropic/Bedrock reject a
|
|
517
|
+
// terminal assistant message whose text ends with whitespace.
|
|
479
518
|
return {
|
|
480
519
|
type: "text" as const,
|
|
481
520
|
text: renderDemotedThinking(model.id, sanitized.thinking),
|
|
521
|
+
[kDemotedThinking]: true,
|
|
482
522
|
};
|
|
483
523
|
}
|
|
484
524
|
|
|
@@ -486,9 +526,11 @@ export function transformMessages<TApi extends Api>(
|
|
|
486
526
|
// Redacted thinking is native-only. Keep it for same-model
|
|
487
527
|
// signed replay, the latest byte-for-byte Anthropic turn, or
|
|
488
528
|
// compatible targets that will also emit sibling unsigned
|
|
489
|
-
// thinking natively. Drop it when the visible thinking
|
|
490
|
-
//
|
|
529
|
+
// thinking natively. Drop it when the matching visible thinking
|
|
530
|
+
// was discarded, or when visible thinking was cross-model
|
|
531
|
+
// stripped and will be demoted to text.
|
|
491
532
|
if (isAnthropicReplay) {
|
|
533
|
+
if (dropsAllSameModelVisibleThinking) return [];
|
|
492
534
|
if (isSameModel || isLatestSurvivingAssistant || replaysUnsignedAnthropicThinking) return block;
|
|
493
535
|
return [];
|
|
494
536
|
}
|
|
@@ -553,6 +595,18 @@ export function transformMessages<TApi extends Api>(
|
|
|
553
595
|
return block;
|
|
554
596
|
});
|
|
555
597
|
|
|
598
|
+
// A demoted-thinking block that survived as the message's final block can
|
|
599
|
+
// still end with the thinking text's own trailing whitespace (bare
|
|
600
|
+
// Anthropic-dialect demotion copies it verbatim), and Anthropic rejects a
|
|
601
|
+
// terminal assistant message whose text ends with trailing whitespace
|
|
602
|
+
// ("final assistant content cannot end with trailing whitespace").
|
|
603
|
+
// trimEnd() is safe: demoted text is synthesized context, never
|
|
604
|
+
// byte-exact replay material.
|
|
605
|
+
const finalBlock = transformedContent[transformedContent.length - 1];
|
|
606
|
+
if (finalBlock?.type === "text" && isDemotedThinking(finalBlock)) {
|
|
607
|
+
transformedContent[transformedContent.length - 1] = { ...finalBlock, text: finalBlock.text.trimEnd() };
|
|
608
|
+
}
|
|
609
|
+
|
|
556
610
|
return {
|
|
557
611
|
...assistantMsg,
|
|
558
612
|
content: transformedContent,
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { createApiKeyLogin } from "../api-key-login";
|
|
9
9
|
|
|
10
|
-
const WAFER_AUTH_URL = "https://wafer.ai/
|
|
10
|
+
const WAFER_AUTH_URL = "https://app.wafer.ai/usage";
|
|
11
11
|
const WAFER_MODELS_URL = "https://pass.wafer.ai/v1/models";
|
|
12
12
|
|
|
13
13
|
export const loginWaferServerless = createApiKeyLogin({
|
package/src/stream.ts
CHANGED
|
@@ -1656,6 +1656,7 @@ function mapOptionsForApi<TApi extends Api>(
|
|
|
1656
1656
|
enabled: true,
|
|
1657
1657
|
level: mapEffortToGoogleThinkingLevel(effort),
|
|
1658
1658
|
},
|
|
1659
|
+
hideThinkingSummary: options?.hideThinkingSummary,
|
|
1659
1660
|
toolChoice: mapGoogleToolChoice(options?.toolChoice),
|
|
1660
1661
|
});
|
|
1661
1662
|
}
|
|
@@ -1666,6 +1667,7 @@ function mapOptionsForApi<TApi extends Api>(
|
|
|
1666
1667
|
enabled: true,
|
|
1667
1668
|
budgetTokens: getGoogleBudget(googleModel, effort, options?.thinkingBudgets),
|
|
1668
1669
|
},
|
|
1670
|
+
hideThinkingSummary: options?.hideThinkingSummary,
|
|
1669
1671
|
toolChoice: mapGoogleToolChoice(options?.toolChoice),
|
|
1670
1672
|
});
|
|
1671
1673
|
}
|
|
@@ -1685,6 +1687,7 @@ function mapOptionsForApi<TApi extends Api>(
|
|
|
1685
1687
|
enabled: true,
|
|
1686
1688
|
level: mapEffortToGoogleThinkingLevel(effort),
|
|
1687
1689
|
},
|
|
1690
|
+
hideThinkingSummary: options?.hideThinkingSummary,
|
|
1688
1691
|
toolChoice,
|
|
1689
1692
|
antigravityEndpointMode: options?.antigravityEndpointMode,
|
|
1690
1693
|
});
|
|
@@ -1707,6 +1710,7 @@ function mapOptionsForApi<TApi extends Api>(
|
|
|
1707
1710
|
maxTokens,
|
|
1708
1711
|
requestModelId: resolveWireModelId(model, effort),
|
|
1709
1712
|
thinking: { enabled: true, budgetTokens: thinkingBudget },
|
|
1713
|
+
hideThinkingSummary: options?.hideThinkingSummary,
|
|
1710
1714
|
toolChoice,
|
|
1711
1715
|
antigravityEndpointMode: options?.antigravityEndpointMode,
|
|
1712
1716
|
});
|
|
@@ -1753,6 +1757,7 @@ function mapOptionsForApi<TApi extends Api>(
|
|
|
1753
1757
|
enabled: true,
|
|
1754
1758
|
level: mapEffortToGoogleThinkingLevel(effort),
|
|
1755
1759
|
},
|
|
1760
|
+
hideThinkingSummary: options?.hideThinkingSummary,
|
|
1756
1761
|
toolChoice: mapGoogleToolChoice(options?.toolChoice),
|
|
1757
1762
|
});
|
|
1758
1763
|
}
|
|
@@ -1764,6 +1769,7 @@ function mapOptionsForApi<TApi extends Api>(
|
|
|
1764
1769
|
enabled: true,
|
|
1765
1770
|
budgetTokens: getGoogleBudget(geminiModel, effort, options?.thinkingBudgets),
|
|
1766
1771
|
},
|
|
1772
|
+
hideThinkingSummary: options?.hideThinkingSummary,
|
|
1767
1773
|
toolChoice: mapGoogleToolChoice(options?.toolChoice),
|
|
1768
1774
|
});
|
|
1769
1775
|
}
|
package/src/types.ts
CHANGED
|
@@ -19,6 +19,7 @@ import type {
|
|
|
19
19
|
WriteResult,
|
|
20
20
|
} from "@oh-my-pi/pi-catalog/discovery/cursor-gen/agent_pb";
|
|
21
21
|
import type { Effort } from "@oh-my-pi/pi-catalog/effort";
|
|
22
|
+
import { isOpenAIModelId } from "@oh-my-pi/pi-catalog/identity/family";
|
|
22
23
|
import type { Api, FetchImpl, KnownApi, Model, Provider, ThinkingBudgets, Usage } from "@oh-my-pi/pi-catalog/types";
|
|
23
24
|
import type { Type } from "arktype";
|
|
24
25
|
import type { ZodType, z } from "zod/v4";
|
|
@@ -134,6 +135,22 @@ export type ServiceTierFamily = "openai" | "anthropic" | "google";
|
|
|
134
135
|
*/
|
|
135
136
|
export type ServiceTierByFamily = Partial<Record<ServiceTierFamily, ServiceTier>>;
|
|
136
137
|
|
|
138
|
+
type ServiceTierModel = Pick<Model, "provider" | "api" | "id">;
|
|
139
|
+
|
|
140
|
+
function isOpenAIServiceTierApi(api: Api | undefined): boolean {
|
|
141
|
+
return api === "openai-completions" || api === "openai-responses" || api === "openai-codex-responses";
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function hasDedicatedServiceTierControl(provider: Provider | undefined): boolean {
|
|
145
|
+
return provider === "fireworks";
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function isOpenAIServiceTierModel(model: ServiceTierModel): boolean {
|
|
149
|
+
return (
|
|
150
|
+
!hasDedicatedServiceTierControl(model.provider) && isOpenAIServiceTierApi(model.api) && isOpenAIModelId(model.id)
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
137
154
|
/**
|
|
138
155
|
* Classify a model into the service-tier family whose knob governs it, or
|
|
139
156
|
* `undefined` when the model exposes no serving-priority control.
|
|
@@ -141,8 +158,10 @@ export type ServiceTierByFamily = Partial<Record<ServiceTierFamily, ServiceTier>
|
|
|
141
158
|
* OpenRouter models are classified by id namespace (`anthropic/`, `google/`,
|
|
142
159
|
* `openai/`); Claude on Bedrock/Vertex (api `anthropic-messages`) is the
|
|
143
160
|
* anthropic family even though its provider is `amazon-bedrock`/`google-vertex`.
|
|
161
|
+
* Custom OpenAI-compatible relays that serve OpenAI model ids are OpenAI family
|
|
162
|
+
* too unless that provider owns a separate tier control such as Fireworks.
|
|
144
163
|
*/
|
|
145
|
-
export function serviceTierFamily(model:
|
|
164
|
+
export function serviceTierFamily(model: ServiceTierModel): ServiceTierFamily | undefined {
|
|
146
165
|
const provider = model.provider;
|
|
147
166
|
if (provider === "openrouter") {
|
|
148
167
|
const id = model.id.toLowerCase();
|
|
@@ -154,6 +173,7 @@ export function serviceTierFamily(model: Pick<Model, "provider" | "api" | "id">)
|
|
|
154
173
|
if (provider === "openai" || provider === "openai-codex") return "openai";
|
|
155
174
|
if (model.api === "anthropic-messages") return "anthropic";
|
|
156
175
|
if (provider === "google" || provider === "google-vertex") return "google";
|
|
176
|
+
if (isOpenAIServiceTierModel(model)) return "openai";
|
|
157
177
|
return undefined;
|
|
158
178
|
}
|
|
159
179
|
|
|
@@ -179,12 +199,16 @@ export function resolveModelServiceTier(
|
|
|
179
199
|
*/
|
|
180
200
|
export function shouldSendServiceTier(
|
|
181
201
|
serviceTier: ServiceTier | null | undefined,
|
|
182
|
-
|
|
202
|
+
target: Provider | ServiceTierModel | undefined,
|
|
183
203
|
): boolean {
|
|
184
204
|
if (!serviceTier) return false;
|
|
205
|
+
const provider = typeof target === "string" ? target : target?.provider;
|
|
185
206
|
if (provider === "openai" || provider === "openai-codex" || provider === "openrouter") {
|
|
186
207
|
return serviceTier === "flex" || serviceTier === "scale" || serviceTier === "priority";
|
|
187
208
|
}
|
|
209
|
+
if (typeof target !== "string" && target && isOpenAIServiceTierModel(target)) {
|
|
210
|
+
return serviceTier === "flex" || serviceTier === "scale" || serviceTier === "priority";
|
|
211
|
+
}
|
|
188
212
|
if (provider === "google") {
|
|
189
213
|
return serviceTier === "flex" || serviceTier === "priority";
|
|
190
214
|
}
|
|
@@ -213,7 +237,7 @@ export function realizesPriorityServiceTier(
|
|
|
213
237
|
return family === "openai" || family === "google";
|
|
214
238
|
}
|
|
215
239
|
if (model.api === "anthropic-messages") return false;
|
|
216
|
-
return shouldSendServiceTier(serviceTier, model
|
|
240
|
+
return shouldSendServiceTier(serviceTier, model);
|
|
217
241
|
}
|
|
218
242
|
|
|
219
243
|
/**
|
|
@@ -46,3 +46,21 @@ export const kCursorExecResolved = Symbol("provider.block.cursorExecResolved");
|
|
|
46
46
|
|
|
47
47
|
/** Carries the resolved marker without exposing a string-keyed property. */
|
|
48
48
|
export type CursorExecResolvedCarrier = object & { [kCursorExecResolved]?: true };
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Marks a text block synthesized by cross-model thinking demotion in
|
|
52
|
+
* `transformMessages`. Converters that flatten adjacent text blocks into one
|
|
53
|
+
* string (openai-completions) insert a paragraph separator after marked
|
|
54
|
+
* blocks; unmarked adjacent blocks keep their original byte sequence.
|
|
55
|
+
* Symbol-keyed so the marker never persists across the JSONL round-trip and
|
|
56
|
+
* never reaches the wire.
|
|
57
|
+
*/
|
|
58
|
+
export const kDemotedThinking = Symbol("provider.block.demotedThinking");
|
|
59
|
+
|
|
60
|
+
/** Carries the demoted-thinking marker without exposing a string-keyed property. */
|
|
61
|
+
export type DemotedThinkingCarrier = object & { [kDemotedThinking]?: boolean };
|
|
62
|
+
|
|
63
|
+
/** True for text blocks synthesized by cross-model thinking demotion. */
|
|
64
|
+
export function isDemotedThinking(block: DemotedThinkingCarrier | null | undefined): boolean {
|
|
65
|
+
return block?.[kDemotedThinking] === true;
|
|
66
|
+
}
|
|
@@ -23,22 +23,50 @@ export type CapturedHttpErrorResponse = {
|
|
|
23
23
|
|
|
24
24
|
const SENSITIVE_HEADERS = ["authorization", "x-api-key", "api-key", "cookie", "set-cookie", "proxy-authorization"];
|
|
25
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Build the JSON persisted for a rejected request. Request fields stay at the
|
|
28
|
+
* top level (so existing dump parsers still read `body`); the provider's error
|
|
29
|
+
* is added under `errorResponse` so a failed request is diagnosable from the
|
|
30
|
+
* dump file rather than the request alone.
|
|
31
|
+
*/
|
|
32
|
+
export function buildHttp400DumpPayload(
|
|
33
|
+
dump: RawHttpRequestDump,
|
|
34
|
+
error: unknown,
|
|
35
|
+
message: string,
|
|
36
|
+
): RawHttpRequestDump & { errorResponse: { status: number | undefined; message: string } } {
|
|
37
|
+
return {
|
|
38
|
+
...sanitizeDump(dump),
|
|
39
|
+
errorResponse: { status: AIError.status(error), message },
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** HTTP statuses whose rejected request we persist for post-hoc diagnosis: the
|
|
44
|
+
* request-content rejections that wedge a session. 400 (bad request) and 413
|
|
45
|
+
* (payload too large — an oversized image / snapcompact frame payload that 413s
|
|
46
|
+
* and empties the turn). Auth (401/403), not-found (404), rate limits and 5xx
|
|
47
|
+
* are excluded: 429/5xx are retried, so persisting them here would write one
|
|
48
|
+
* dump per attempt. */
|
|
49
|
+
export function shouldDumpRejectedRequest(error: unknown): boolean {
|
|
50
|
+
const status = AIError.status(error);
|
|
51
|
+
return status === 400 || status === 413;
|
|
52
|
+
}
|
|
53
|
+
|
|
26
54
|
export async function appendRawHttpRequestDumpFor400(
|
|
27
55
|
message: string,
|
|
28
56
|
error: unknown,
|
|
29
57
|
dump: RawHttpRequestDump | undefined,
|
|
30
58
|
): Promise<string> {
|
|
31
59
|
// Never persist dumps under the test runner: providers exercise the 400 path
|
|
32
|
-
if (!dump || isBunTestRuntime() ||
|
|
60
|
+
if (!dump || isBunTestRuntime() || !shouldDumpRejectedRequest(error)) {
|
|
33
61
|
return message;
|
|
34
62
|
}
|
|
35
63
|
|
|
36
|
-
const
|
|
37
|
-
const fileName = `${Date.now()}-${Bun.hash(JSON.stringify(
|
|
64
|
+
const payload = buildHttp400DumpPayload(dump, error, message);
|
|
65
|
+
const fileName = `${Date.now()}-${Bun.hash(JSON.stringify(payload)).toString(36)}.json`;
|
|
38
66
|
const filePath = path.join(getLogsDir(), "http-400-requests", fileName);
|
|
39
67
|
|
|
40
68
|
try {
|
|
41
|
-
await Bun.write(filePath, `${JSON.stringify(
|
|
69
|
+
await Bun.write(filePath, `${JSON.stringify(payload, null, 2)}\n`);
|
|
42
70
|
return `${message}\nraw-http-request=${filePath}`;
|
|
43
71
|
} catch (writeError) {
|
|
44
72
|
const writeMessage = writeError instanceof Error ? writeError.message : String(writeError);
|