@gajae-code/ai 0.13.0 → 0.13.2
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 +6 -0
- package/dist/types/openai-completions-compat.d.ts +2 -1
- package/dist/types/providers/openai-responses-shared.d.ts +1 -1
- package/dist/types/types.d.ts +17 -3
- package/package.json +2 -2
- package/src/openai-completions-compat.ts +3 -0
- package/src/providers/anthropic.ts +61 -0
- package/src/providers/openai-completions.ts +1 -1
- package/src/providers/openai-opencodex-responses.ts +1 -0
- package/src/providers/openai-responses-shared.ts +2 -1
- package/src/providers/openai-responses.ts +1 -1
- package/src/providers/transform-messages.ts +9 -2
- package/src/types.ts +36 -9
package/CHANGELOG.md
CHANGED
|
@@ -2,9 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.13.2] - 2026-08-13
|
|
6
|
+
|
|
7
|
+
## [0.13.1] - 2026-08-11
|
|
8
|
+
|
|
5
9
|
### Fixed
|
|
6
10
|
|
|
7
11
|
- Anthropic thinking-replay repairs caused by a deterministic rejection now stay in force for the rest of the session instead of being released as soon as the repaired retry succeeds. `thinking`/`redacted_thinking` blocks that draw a 400 (`blocks ... cannot be modified`, `Invalid \`signature\` in \`thinking\` block`) stay in the session history, so releasing the repair made the very next turn replay the same blocks and spend another rejected round trip — every turn, indefinitely. Observed against a proxied Claude Code session as a sustained ~50% 400 rate that never converged. The speculative masked-`api_error` probe is still released on the first completed stream, since that one may have been a transient blip (#4011).
|
|
12
|
+
- Anthropic requests no longer spend a rejected round trip discovering that the latest assistant turn cannot replay its thinking block. Anthropic streams a `thinking` block as a start/stop pair with no `thinking_delta` and no `signature_delta` when it withholds reasoning, which lands in history empty and unsigned; `convertAnthropicMessages` then dropped it, and the turn went back carrying only its `tool_use`. Anthropic validates the latest assistant message against the turn it produced and rejects the missing block with 400 `blocks ... cannot be modified`. A captured production request showed exactly this shape — 1214 messages, 29 signed-but-empty thinking blocks preserved, and the latest assistant turn reduced to a bare `tool_use`. The condition is visible locally, so the replay now degrades during the first build instead of after the rejection.
|
|
13
|
+
- The unreplayable-thinking detection now correctly excludes non-signing endpoints (DeepSeek, Z.AI), which replay unsigned blocks verbatim and never validate thinking presence, and signed-but-empty blocks, which `convertAnthropicMessages` forwards natively via the signed-thinking path. The original detection treated empty text as always unreplayable, which degraded valid reasoning on non-signing endpoints and dropped natively replayable signed-empty blocks (#4172).
|
|
8
14
|
|
|
9
15
|
## [0.12.21] - 2026-08-09
|
|
10
16
|
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import type { Model, OpenAICompat } from "./types";
|
|
2
2
|
type ResolvedToolStrictMode = NonNullable<OpenAICompat["toolStrictMode"]> | "mixed";
|
|
3
|
-
export type ResolvedOpenAICompat = Required<Omit<OpenAICompat, "openRouterRouting" | "vercelGatewayRouting" | "extraBody" | "toolStrictMode" | "toolChoiceSupport" | "supportsResponsesSessionAffinity" | "reservedToolNames">> & {
|
|
3
|
+
export type ResolvedOpenAICompat = Required<Omit<OpenAICompat, "openRouterRouting" | "vercelGatewayRouting" | "extraBody" | "toolStrictMode" | "toolChoiceSupport" | "supportsResponsesSessionAffinity" | "supportsServiceTier" | "reservedToolNames">> & {
|
|
4
4
|
openRouterRouting?: OpenAICompat["openRouterRouting"];
|
|
5
5
|
vercelGatewayRouting?: OpenAICompat["vercelGatewayRouting"];
|
|
6
6
|
extraBody?: OpenAICompat["extraBody"];
|
|
7
7
|
toolStrictMode: ResolvedToolStrictMode;
|
|
8
8
|
supportsResponsesSessionAffinity?: OpenAICompat["supportsResponsesSessionAffinity"];
|
|
9
|
+
supportsServiceTier?: OpenAICompat["supportsServiceTier"];
|
|
9
10
|
/** Optional explicit capability override; resolved via deriveToolChoiceSupport. */
|
|
10
11
|
toolChoiceSupport?: OpenAICompat["toolChoiceSupport"];
|
|
11
12
|
};
|
|
@@ -79,7 +79,7 @@ type CommonSamplingOptions = Pick<StreamOptions, "temperature" | "topP" | "topK"
|
|
|
79
79
|
* Apply the common `StreamOptions` → Responses sampling-parameter mapping (max output tokens,
|
|
80
80
|
* temperature, top-p/k, min-p, presence/repetition penalties, service tier). Mutates `params`.
|
|
81
81
|
*/
|
|
82
|
-
export declare function applyCommonResponsesSamplingParams<P extends CommonResponsesParams>(params: P, options: CommonSamplingOptions | undefined, provider: string): void;
|
|
82
|
+
export declare function applyCommonResponsesSamplingParams<P extends CommonResponsesParams>(params: P, options: CommonSamplingOptions | undefined, provider: string, supportsServiceTier?: boolean): void;
|
|
83
83
|
type ReasoningOptions = {
|
|
84
84
|
reasoning?: string;
|
|
85
85
|
reasoningSummary?: "auto" | "detailed" | "concise" | null;
|
package/dist/types/types.d.ts
CHANGED
|
@@ -111,10 +111,16 @@ export type ResolvedServiceTier = Exclude<ServiceTier, "openai-only" | "claude-o
|
|
|
111
111
|
export declare function resolveServiceTier(serviceTier: ServiceTier | null | undefined, provider: Provider | undefined): ResolvedServiceTier | undefined;
|
|
112
112
|
/**
|
|
113
113
|
* True when the (possibly scoped) tier should be sent as an OpenAI-compatible
|
|
114
|
-
* `service_tier` request field
|
|
115
|
-
*
|
|
114
|
+
* `service_tier` request field. Custom providers must explicitly opt in through
|
|
115
|
+
* `compat.supportsServiceTier`; unknown providers remain fail-closed.
|
|
116
116
|
*/
|
|
117
|
-
export declare function shouldSendServiceTier(serviceTier: ServiceTier | null | undefined, provider: Provider | undefined): boolean;
|
|
117
|
+
export declare function shouldSendServiceTier(serviceTier: ServiceTier | null | undefined, provider: Provider | undefined, supportsServiceTier?: boolean): boolean;
|
|
118
|
+
/**
|
|
119
|
+
* True when a priority tier is realized as a fast-mode request on the provider's
|
|
120
|
+
* wire protocol. Custom OpenAI-compatible proxies opt in explicitly rather than
|
|
121
|
+
* inheriting support merely because their API shape resembles OpenAI.
|
|
122
|
+
*/
|
|
123
|
+
export declare function isFastModeEffectiveForProvider(serviceTier: ServiceTier | null | undefined, provider: Provider | undefined, supportsServiceTier?: boolean): boolean;
|
|
118
124
|
/**
|
|
119
125
|
* Premium-request weight contributed by sending priority to a provider
|
|
120
126
|
* that supports it. Mirrors GitHub Copilot's `premiumRequests` accounting
|
|
@@ -718,6 +724,12 @@ export interface OpenAICompat extends ToolChoiceCompat {
|
|
|
718
724
|
* HTTPS origin automatically; known non-OpenAI providers remain excluded.
|
|
719
725
|
*/
|
|
720
726
|
supportsResponsesSessionAffinity?: boolean;
|
|
727
|
+
/**
|
|
728
|
+
* Whether an OpenAI-compatible endpoint accepts the `service_tier` request
|
|
729
|
+
* field. Disabled by default for custom providers; opt in only when the proxy
|
|
730
|
+
* preserves or intentionally realizes OpenAI priority processing.
|
|
731
|
+
*/
|
|
732
|
+
supportsServiceTier?: boolean;
|
|
721
733
|
/**
|
|
722
734
|
* Tool names the provider reserves for its own built-ins and refuses to
|
|
723
735
|
* accept as custom function declarations. A colliding tool is **dropped**
|
|
@@ -957,3 +969,5 @@ export interface Model<TApi extends Api = any> {
|
|
|
957
969
|
*/
|
|
958
970
|
isOAuth?: boolean;
|
|
959
971
|
}
|
|
972
|
+
/** True when a model explicitly opts into OpenAI-compatible `service_tier` forwarding. */
|
|
973
|
+
export declare function modelSupportsServiceTier(model: Pick<Model, "compat"> | undefined): boolean;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@gajae-code/ai",
|
|
4
|
-
"version": "0.13.
|
|
4
|
+
"version": "0.13.2",
|
|
5
5
|
"description": "Unified LLM API with automatic model discovery and provider configuration",
|
|
6
6
|
"homepage": "https://gajae-code.com",
|
|
7
7
|
"author": "Yeachan-Heo and Gajae Code Contributors",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"dependencies": {
|
|
41
41
|
"@anthropic-ai/sdk": "^0.94.0",
|
|
42
42
|
"@bufbuild/protobuf": "^2.12.0",
|
|
43
|
-
"@gajae-code/utils": "0.13.
|
|
43
|
+
"@gajae-code/utils": "0.13.2",
|
|
44
44
|
"openai": "^6.36.0",
|
|
45
45
|
"partial-json": "^0.1.7",
|
|
46
46
|
"zod": "4.4.3"
|
|
@@ -12,6 +12,7 @@ export type ResolvedOpenAICompat = Required<
|
|
|
12
12
|
| "toolStrictMode"
|
|
13
13
|
| "toolChoiceSupport"
|
|
14
14
|
| "supportsResponsesSessionAffinity"
|
|
15
|
+
| "supportsServiceTier"
|
|
15
16
|
| "reservedToolNames"
|
|
16
17
|
>
|
|
17
18
|
> & {
|
|
@@ -20,6 +21,7 @@ export type ResolvedOpenAICompat = Required<
|
|
|
20
21
|
extraBody?: OpenAICompat["extraBody"];
|
|
21
22
|
toolStrictMode: ResolvedToolStrictMode;
|
|
22
23
|
supportsResponsesSessionAffinity?: OpenAICompat["supportsResponsesSessionAffinity"];
|
|
24
|
+
supportsServiceTier?: OpenAICompat["supportsServiceTier"];
|
|
23
25
|
/** Optional explicit capability override; resolved via deriveToolChoiceSupport. */
|
|
24
26
|
toolChoiceSupport?: OpenAICompat["toolChoiceSupport"];
|
|
25
27
|
};
|
|
@@ -282,6 +284,7 @@ export function resolveOpenAICompat(
|
|
|
282
284
|
("supportsResponsesSessionAffinity" in model.compat
|
|
283
285
|
? model.compat.supportsResponsesSessionAffinity
|
|
284
286
|
: undefined) ?? detected.supportsResponsesSessionAffinity,
|
|
287
|
+
supportsServiceTier: model.compat.supportsServiceTier ?? detected.supportsServiceTier,
|
|
285
288
|
supportsMultipleSystemMessages:
|
|
286
289
|
model.compat.supportsMultipleSystemMessages ?? detected.supportsMultipleSystemMessages,
|
|
287
290
|
supportsReasoningEffort: model.compat.supportsReasoningEffort ?? detected.supportsReasoningEffort,
|
|
@@ -2369,6 +2369,46 @@ function hasNativeThinkingBlocks(messages: MessageParam[]): boolean {
|
|
|
2369
2369
|
);
|
|
2370
2370
|
}
|
|
2371
2371
|
|
|
2372
|
+
/**
|
|
2373
|
+
* Would the latest assistant turn lose a thinking block on its way to the wire?
|
|
2374
|
+
*
|
|
2375
|
+
* `convertAnthropicMessages` can only replay a `thinking` block natively when it
|
|
2376
|
+
* still carries the bytes Anthropic signed. A block that arrived as a bare
|
|
2377
|
+
* start/stop pair — no `thinking_delta`, no `signature_delta` — has neither, so
|
|
2378
|
+
* it is silently dropped, and Anthropic rejects the turn it produced for coming
|
|
2379
|
+
* back without it. Same for a `redactedThinking` block whose opaque payload is
|
|
2380
|
+
* gone. Only the latest assistant message is inspected because that is the turn
|
|
2381
|
+
* Anthropic validates against its own output.
|
|
2382
|
+
*/
|
|
2383
|
+
function latestAssistantThinkingIsUnreplayable(messages: Message[], model: Model<"anthropic-messages">): boolean {
|
|
2384
|
+
const index = messages.findLastIndex(message => message.role === "assistant");
|
|
2385
|
+
if (index < 0) return false;
|
|
2386
|
+
|
|
2387
|
+
const assistant = messages[index] as AssistantMessage;
|
|
2388
|
+
// Cross-API history degrades to text rather than replaying native blocks, so
|
|
2389
|
+
// nothing is lost and nothing needs repairing.
|
|
2390
|
+
if (assistant.api !== "anthropic-messages") return false;
|
|
2391
|
+
// Endpoints that never sign thinking replay unsigned blocks verbatim.
|
|
2392
|
+
const requiresSignature = !isNonSigningAnthropicEndpoint(model);
|
|
2393
|
+
|
|
2394
|
+
return assistant.content.some(block => {
|
|
2395
|
+
if (block.type === "redactedThinking") return block.data.trim().length === 0;
|
|
2396
|
+
if (block.type !== "thinking") return false;
|
|
2397
|
+
// A block with empty text and no signature cannot go back on the wire:
|
|
2398
|
+
// `convertAnthropicMessages` drops it, and Anthropic rejects the turn for
|
|
2399
|
+
// arriving without it. A block with a valid signature AND non-empty text is
|
|
2400
|
+
// replayable. But a signed block whose text was emptied — e.g. by
|
|
2401
|
+
// clear_thinking_20251015 — carries a stale signature that signing endpoints
|
|
2402
|
+
// reject on replay (issue #4247). Non-signing endpoints replay unsigned
|
|
2403
|
+
// blocks verbatim, so only they treat a missing signature as unreplayable.
|
|
2404
|
+
const hasSignature = !!block.thinkingSignature?.trim();
|
|
2405
|
+
const isEmpty = !block.thinking.trim();
|
|
2406
|
+
if (!hasSignature) return requiresSignature;
|
|
2407
|
+
if (isEmpty && requiresSignature) return true;
|
|
2408
|
+
return false;
|
|
2409
|
+
});
|
|
2410
|
+
}
|
|
2411
|
+
|
|
2372
2412
|
function mapAnthropicToolChoice(
|
|
2373
2413
|
toolChoice: NonNullable<ResolveToolChoiceResult["resolvedChoice"]>,
|
|
2374
2414
|
isOAuthToken: boolean,
|
|
@@ -2727,6 +2767,27 @@ function buildParams(
|
|
|
2727
2767
|
});
|
|
2728
2768
|
}
|
|
2729
2769
|
|
|
2770
|
+
// Anthropic compares the latest assistant message against the turn it actually
|
|
2771
|
+
// produced, and rejects it when a `thinking`/`redacted_thinking` block that was
|
|
2772
|
+
// in that response is missing. A block Anthropic streamed as a start/stop pair
|
|
2773
|
+
// with no `thinking_delta` and no `signature_delta` lands in history empty and
|
|
2774
|
+
// unsigned, and `convertAnthropicMessages` then drops it: the turn goes back
|
|
2775
|
+
// carrying only its `tool_use`, and the request is rejected before a token
|
|
2776
|
+
// streams. The rejection is recoverable — the repair drops native thinking from
|
|
2777
|
+
// the whole replay — but only after a full round trip has been spent, and the
|
|
2778
|
+
// condition is visible locally, so detect it here and degrade in the first
|
|
2779
|
+
// build instead of paying for the 400 to discover it.
|
|
2780
|
+
if (
|
|
2781
|
+
!thinkingRepair?.repairAllAssistantThinking &&
|
|
2782
|
+
latestAssistantThinkingIsUnreplayable(context.messages, model) &&
|
|
2783
|
+
hasNativeThinkingBlocks(params.messages)
|
|
2784
|
+
) {
|
|
2785
|
+
params.messages = convertAnthropicMessages(context.messages, model, isOAuthToken, {
|
|
2786
|
+
...thinkingRepair,
|
|
2787
|
+
repairAllAssistantThinking: true,
|
|
2788
|
+
});
|
|
2789
|
+
}
|
|
2790
|
+
|
|
2730
2791
|
const shouldInjectClaudeCodeInstruction = isOAuthToken && !model.id.startsWith("claude-3-5-haiku");
|
|
2731
2792
|
const billingSystemPrompts = normalizeSystemPrompts(context.systemPrompt);
|
|
2732
2793
|
const billingPayload = shouldInjectClaudeCodeInstruction
|
|
@@ -1335,7 +1335,7 @@ function buildParams(
|
|
|
1335
1335
|
if (options?.frequencyPenalty !== undefined) {
|
|
1336
1336
|
params.frequency_penalty = options.frequencyPenalty;
|
|
1337
1337
|
}
|
|
1338
|
-
if (shouldSendServiceTier(options?.serviceTier, model.provider)) {
|
|
1338
|
+
if (shouldSendServiceTier(options?.serviceTier, model.provider, compat.supportsServiceTier === true)) {
|
|
1339
1339
|
const resolved = resolveServiceTier(options?.serviceTier, model.provider);
|
|
1340
1340
|
if (resolved === "flex" || resolved === "scale" || resolved === "priority") {
|
|
1341
1341
|
params.service_tier = resolved;
|
|
@@ -141,6 +141,7 @@ function normalizeModel(row: CatalogRow, endpoint: OpenCodexEndpoint): Model<"op
|
|
|
141
141
|
api: "openai-responses",
|
|
142
142
|
provider: "opencodex",
|
|
143
143
|
baseUrl: `${endpoint.baseUrl}/v1`,
|
|
144
|
+
compat: { supportsServiceTier: true },
|
|
144
145
|
reasoning: row.reasoning !== false,
|
|
145
146
|
input,
|
|
146
147
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
@@ -920,6 +920,7 @@ export function applyCommonResponsesSamplingParams<P extends CommonResponsesPara
|
|
|
920
920
|
params: P,
|
|
921
921
|
options: CommonSamplingOptions | undefined,
|
|
922
922
|
provider: string,
|
|
923
|
+
supportsServiceTier = false,
|
|
923
924
|
): void {
|
|
924
925
|
if (options?.maxTokens) params.max_output_tokens = options.maxTokens;
|
|
925
926
|
if (options?.temperature !== undefined) params.temperature = options.temperature;
|
|
@@ -928,7 +929,7 @@ export function applyCommonResponsesSamplingParams<P extends CommonResponsesPara
|
|
|
928
929
|
if (options?.minP !== undefined) params.min_p = options.minP;
|
|
929
930
|
if (options?.presencePenalty !== undefined) params.presence_penalty = options.presencePenalty;
|
|
930
931
|
if (options?.repetitionPenalty !== undefined) params.repetition_penalty = options.repetitionPenalty;
|
|
931
|
-
if (shouldSendServiceTier(options?.serviceTier, provider)) {
|
|
932
|
+
if (shouldSendServiceTier(options?.serviceTier, provider, supportsServiceTier)) {
|
|
932
933
|
const resolved = resolveServiceTier(options?.serviceTier, provider);
|
|
933
934
|
if (resolved === "flex" || resolved === "scale" || resolved === "priority") {
|
|
934
935
|
params.service_tier = resolved;
|
|
@@ -709,7 +709,7 @@ function buildParams(
|
|
|
709
709
|
stream_options: model.provider === "openai" ? { include_obfuscation: false } : undefined,
|
|
710
710
|
};
|
|
711
711
|
|
|
712
|
-
applyCommonResponsesSamplingParams(params, options, model.provider);
|
|
712
|
+
applyCommonResponsesSamplingParams(params, options, model.provider, model.compat?.supportsServiceTier === true);
|
|
713
713
|
// TODO: openai responses has no top-level `stop`/`stop_sequences`; surface via reasoning.stop?
|
|
714
714
|
// `StreamOptions.stopSequences` is intentionally dropped for this provider.
|
|
715
715
|
// TODO: openai responses has no top-level `frequency_penalty` field as of the current SDK;
|
|
@@ -98,8 +98,15 @@ export function transformMessages<TApi extends Api>(
|
|
|
98
98
|
if (dropAssistantThinkingForRepair && replaysAsNativeThinking) return [];
|
|
99
99
|
if (mustPreserveLatestAnthropicThinking) return sanitized;
|
|
100
100
|
// For same model: keep thinking blocks with signatures (needed for replay)
|
|
101
|
-
// even if the thinking text is empty
|
|
102
|
-
|
|
101
|
+
// even if the thinking text is empty — but only for non-Anthropic APIs where
|
|
102
|
+
// the signature represents OpenAI encrypted reasoning. For anthropic-messages,
|
|
103
|
+
// a signed block with empty text means clear_thinking_20251015 stripped the
|
|
104
|
+
// content server-side while the stale signature remained; replaying it
|
|
105
|
+
// produces `thinking ... cannot be modified` 400s on every turn (#4247).
|
|
106
|
+
if (isSameModel && sanitized.thinkingSignature) {
|
|
107
|
+
if (sanitized.thinking.trim() === "" && model.api === "anthropic-messages") return [];
|
|
108
|
+
return sanitized;
|
|
109
|
+
}
|
|
103
110
|
// Skip empty thinking blocks, convert others to plain text
|
|
104
111
|
if (!sanitized.thinking || sanitized.thinking.trim() === "") return [];
|
|
105
112
|
if (isSameModel) return sanitized;
|
package/src/types.ts
CHANGED
|
@@ -257,19 +257,40 @@ export function resolveServiceTier(
|
|
|
257
257
|
|
|
258
258
|
/**
|
|
259
259
|
* True when the (possibly scoped) tier should be sent as an OpenAI-compatible
|
|
260
|
-
* `service_tier` request field
|
|
261
|
-
*
|
|
260
|
+
* `service_tier` request field. Custom providers must explicitly opt in through
|
|
261
|
+
* `compat.supportsServiceTier`; unknown providers remain fail-closed.
|
|
262
262
|
*/
|
|
263
263
|
export function shouldSendServiceTier(
|
|
264
264
|
serviceTier: ServiceTier | null | undefined,
|
|
265
265
|
provider: Provider | undefined,
|
|
266
|
+
supportsServiceTier = false,
|
|
266
267
|
): boolean {
|
|
267
268
|
const resolved = resolveServiceTier(serviceTier, provider);
|
|
268
269
|
if (provider === "deepinfra") return resolved === "priority";
|
|
269
|
-
if (provider !== "openai" && provider !== "openai-codex") return false;
|
|
270
|
+
if (provider !== "openai" && provider !== "openai-codex" && !supportsServiceTier) return false;
|
|
270
271
|
return resolved === "flex" || resolved === "scale" || resolved === "priority";
|
|
271
272
|
}
|
|
272
273
|
|
|
274
|
+
/**
|
|
275
|
+
* True when a priority tier is realized as a fast-mode request on the provider's
|
|
276
|
+
* wire protocol. Custom OpenAI-compatible proxies opt in explicitly rather than
|
|
277
|
+
* inheriting support merely because their API shape resembles OpenAI.
|
|
278
|
+
*/
|
|
279
|
+
export function isFastModeEffectiveForProvider(
|
|
280
|
+
serviceTier: ServiceTier | null | undefined,
|
|
281
|
+
provider: Provider | undefined,
|
|
282
|
+
supportsServiceTier = false,
|
|
283
|
+
): boolean {
|
|
284
|
+
if (resolveServiceTier(serviceTier, provider) !== "priority") return false;
|
|
285
|
+
return (
|
|
286
|
+
provider === "openai" ||
|
|
287
|
+
provider === "openai-codex" ||
|
|
288
|
+
provider === "anthropic" ||
|
|
289
|
+
provider === "deepinfra" ||
|
|
290
|
+
supportsServiceTier
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
|
|
273
294
|
/**
|
|
274
295
|
* Premium-request weight contributed by sending priority to a provider
|
|
275
296
|
* that supports it. Mirrors GitHub Copilot's `premiumRequests` accounting
|
|
@@ -282,12 +303,7 @@ export function getPriorityPremiumRequests(
|
|
|
282
303
|
serviceTier: ServiceTier | null | undefined,
|
|
283
304
|
provider: Provider | undefined,
|
|
284
305
|
): number {
|
|
285
|
-
|
|
286
|
-
// Only providers that realize `priority` on the wire bill the user.
|
|
287
|
-
// Everywhere else, the field is silently dropped and nothing is charged.
|
|
288
|
-
return provider === "openai" || provider === "openai-codex" || provider === "anthropic" || provider === "deepinfra"
|
|
289
|
-
? 1
|
|
290
|
-
: 0;
|
|
306
|
+
return isFastModeEffectiveForProvider(serviceTier, provider) ? 1 : 0;
|
|
291
307
|
}
|
|
292
308
|
|
|
293
309
|
export interface ProviderSessionState {
|
|
@@ -887,6 +903,12 @@ export interface OpenAICompat extends ToolChoiceCompat {
|
|
|
887
903
|
* HTTPS origin automatically; known non-OpenAI providers remain excluded.
|
|
888
904
|
*/
|
|
889
905
|
supportsResponsesSessionAffinity?: boolean;
|
|
906
|
+
/**
|
|
907
|
+
* Whether an OpenAI-compatible endpoint accepts the `service_tier` request
|
|
908
|
+
* field. Disabled by default for custom providers; opt in only when the proxy
|
|
909
|
+
* preserves or intentionally realizes OpenAI priority processing.
|
|
910
|
+
*/
|
|
911
|
+
supportsServiceTier?: boolean;
|
|
890
912
|
/**
|
|
891
913
|
* Tool names the provider reserves for its own built-ins and refuses to
|
|
892
914
|
* accept as custom function declarations. A colliding tool is **dropped**
|
|
@@ -1148,3 +1170,8 @@ export interface Model<TApi extends Api = any> {
|
|
|
1148
1170
|
*/
|
|
1149
1171
|
isOAuth?: boolean;
|
|
1150
1172
|
}
|
|
1173
|
+
|
|
1174
|
+
/** True when a model explicitly opts into OpenAI-compatible `service_tier` forwarding. */
|
|
1175
|
+
export function modelSupportsServiceTier(model: Pick<Model, "compat"> | undefined): boolean {
|
|
1176
|
+
return Boolean(model?.compat && "supportsServiceTier" in model.compat && model.compat.supportsServiceTier === true);
|
|
1177
|
+
}
|