@oh-my-pi/pi-ai 16.1.15 → 16.1.17
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 +24 -0
- package/dist/types/auth-broker/wire-schemas.d.ts +1 -0
- package/dist/types/providers/openai-responses.d.ts +5 -0
- package/dist/types/usage.d.ts +8 -0
- package/package.json +4 -4
- package/src/auth-broker/wire-schemas.ts +1 -0
- package/src/auth-storage.ts +8 -2
- package/src/providers/anthropic.ts +75 -20
- package/src/providers/ollama.ts +21 -1
- package/src/providers/openai-responses.ts +16 -1
- package/src/stream.ts +9 -2
- package/src/usage/opencode-go.ts +1 -1
- package/src/usage.ts +8 -0
- package/src/utils.ts +14 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,30 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [16.1.17] - 2026-06-24
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Added provider-level `notes?: string[]` field to `UsageReport` for disclaimers that apply to every limit (e.g. "OMP-observed spend only"). The field is declared in both the `usage.ts` schema and the auth-broker wire schema copy so it survives the `"+": "reject"` deserialization gate. ([#3268](https://github.com/can1357/oh-my-pi/issues/3268))
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- Moved the OpenCode Go "OMP-observed spend only" disclaimer from per-limit `notes` to provider-level `notes`, so it renders once per provider instead of duplicating across every account × window. ([#3268](https://github.com/can1357/oh-my-pi/issues/3268))
|
|
14
|
+
- Fixed Anthropic rate-limit header usage cache entries retaining legacy missing account metadata after refresh.
|
|
15
|
+
- Fixed Anthropic-compatible budget-effort models dropping the selected effort before request serialization, so `output_config.effort` is emitted alongside `thinking.budget_tokens` when model metadata declares `mode: "anthropic-budget-effort"`.
|
|
16
|
+
- Fixed `anthropic-messages` silently dropping caller-supplied `Authorization` / `X-Api-Key` from `model.headers` and `ANTHROPIC_CUSTOM_HEADERS`, blocking custom proxy auth schemes. Non-OAuth requests now honor the caller's value (matching `openai-responses`); the lower-level client also suppresses its `X-Api-Key` add when a custom `Authorization` is supplied for a non-official endpoint so the proxy receives a single credential. OAuth bearer + Cloudflare AI Gateway keep their pre-existing enforced auth headers. ([#3391](https://github.com/can1357/oh-my-pi/issues/3391))
|
|
17
|
+
- Fixed Ollama Cloud `num_predict` ignoring the provider's 65536 output-token cap so stale `models.db` rows (or custom `modelOverrides` re-enabling output caps) that carried `maxTokens: 1048576` from a pre-omitMaxOutputTokens catalog 400'd every request with `max_tokens (1048576) exceeds model's maximum output tokens (65536) for model deepseek-v4-pro`. The Ollama provider now clamps `num_predict` for any `ollama-cloud` request at the documented 65536 cap before sending, independent of the cached spec's `maxTokens` and on top of the existing `omitMaxOutputTokens` policy — so the request stays valid even when the load-time policy never normalized the spec. Self-hosted `ollama` traffic is unaffected. ([#3392](https://github.com/can1357/oh-my-pi/issues/3392))
|
|
18
|
+
- Fixed OpenRouter Anthropic models on the Responses path omitting `cache_control`, so prompt caching engages without forcing Chat Completions. ([#3397](https://github.com/can1357/oh-my-pi/issues/3397))
|
|
19
|
+
- Fixed OpenRouter Anthropic Responses follow-up requests replaying prior reasoning items with stale signatures, which caused HTTP 400 `Invalid signature in thinking block` errors after a thinking turn. ([#3399](https://github.com/can1357/oh-my-pi/issues/3399))
|
|
20
|
+
- Fixed OpenRouter Anthropic models on the Responses path omitting `cache_control`, so prompt caching engages without forcing Chat Completions. `cacheRetention: "long"` now upgrades the breakpoint to `ttl: "1h"`. ([#3397](https://github.com/can1357/oh-my-pi/issues/3397))
|
|
21
|
+
|
|
22
|
+
## [16.1.16] - 2026-06-23
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
|
|
26
|
+
- Fixed Anthropic-compatible thinking requests sending replayed thinking blocks without `context_management.keep: "all"`, preserving multi-turn reasoning context for API-key providers. API-key requests now also advertise the required `context-management-2025-06-27` beta header so the field is honored instead of rejected. Injected SDK clients, GitHub Copilot's Anthropic proxy, and Vertex rawPredict are excluded because this code path cannot add the beta to caller-owned clients, Copilot strips Anthropic betas and demotes thinking blocks to text upstream, and Vertex expects betas in the JSON body rather than the Anthropic HTTP beta header. ([#3288](https://github.com/can1357/oh-my-pi/issues/3288))
|
|
27
|
+
- Fixed OpenRouter Responses native history replay leaking Gemini reasoning item `format` metadata back into follow-up requests, which caused HTTP 400 rejections while preserving encrypted reasoning replay.
|
|
28
|
+
|
|
5
29
|
## [16.1.15] - 2026-06-22
|
|
6
30
|
|
|
7
31
|
### Fixed
|
|
@@ -76,6 +76,10 @@ interface OpenAIResponsesChainState {
|
|
|
76
76
|
/** Set once chaining is judged unsupported for this session (circuit breaker). */
|
|
77
77
|
disabled: boolean;
|
|
78
78
|
}
|
|
79
|
+
type OpenRouterAnthropicCacheControl = {
|
|
80
|
+
type: "ephemeral";
|
|
81
|
+
ttl?: "1h";
|
|
82
|
+
};
|
|
79
83
|
type OpenAIResponsesSamplingParams = ResponseCreateParamsStreaming & {
|
|
80
84
|
top_p?: number;
|
|
81
85
|
top_k?: number;
|
|
@@ -92,6 +96,7 @@ type OpenAIResponsesSamplingParams = ResponseCreateParamsStreaming & {
|
|
|
92
96
|
} | {
|
|
93
97
|
enabled: false;
|
|
94
98
|
};
|
|
99
|
+
cache_control?: OpenRouterAnthropicCacheControl;
|
|
95
100
|
};
|
|
96
101
|
/**
|
|
97
102
|
* Public entry: wrap the single-attempt Responses streamer with bounded
|
package/dist/types/usage.d.ts
CHANGED
|
@@ -68,6 +68,13 @@ export interface UsageReport {
|
|
|
68
68
|
limits: UsageLimit[];
|
|
69
69
|
/** Saved rate-limit resets the account can redeem, when the provider reports them. */
|
|
70
70
|
resetCredits?: UsageResetCredits;
|
|
71
|
+
/**
|
|
72
|
+
* Provider-wide disclaimers shown once above per-account sections.
|
|
73
|
+
* Use this for caveats that apply to every limit (e.g. "OMP-observed
|
|
74
|
+
* spend only"). Per-limit notes that differ per window (e.g. "Overage
|
|
75
|
+
* requests: N") stay on {@link UsageLimit.notes}.
|
|
76
|
+
*/
|
|
77
|
+
notes?: string[];
|
|
71
78
|
metadata?: Record<string, unknown>;
|
|
72
79
|
raw?: unknown;
|
|
73
80
|
}
|
|
@@ -220,6 +227,7 @@ export declare const usageReportSchema: import("arktype/internal/variants/object
|
|
|
220
227
|
resetCredits?: {
|
|
221
228
|
availableCount: number;
|
|
222
229
|
} | undefined;
|
|
230
|
+
notes?: string[] | undefined;
|
|
223
231
|
metadata?: {
|
|
224
232
|
[x: string]: unknown;
|
|
225
233
|
} | undefined;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-ai",
|
|
4
|
-
"version": "16.1.
|
|
4
|
+
"version": "16.1.17",
|
|
5
5
|
"description": "Unified LLM API with automatic model discovery and provider configuration",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -38,9 +38,9 @@
|
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
40
|
"@bufbuild/protobuf": "^2.12.0",
|
|
41
|
-
"@oh-my-pi/pi-catalog": "16.1.
|
|
42
|
-
"@oh-my-pi/pi-utils": "16.1.
|
|
43
|
-
"@oh-my-pi/pi-wire": "16.1.
|
|
41
|
+
"@oh-my-pi/pi-catalog": "16.1.17",
|
|
42
|
+
"@oh-my-pi/pi-utils": "16.1.17",
|
|
43
|
+
"@oh-my-pi/pi-wire": "16.1.17",
|
|
44
44
|
"arktype": "^2.2.0",
|
|
45
45
|
"zod": "^4"
|
|
46
46
|
},
|
package/src/auth-storage.ts
CHANGED
|
@@ -2326,8 +2326,13 @@ export class AuthStorage {
|
|
|
2326
2326
|
const last = this.#usageHeaderIngestAt.get(cacheKey);
|
|
2327
2327
|
if (last !== undefined && now - last < USAGE_HEADER_INGEST_INTERVAL_MS) return false;
|
|
2328
2328
|
|
|
2329
|
-
const
|
|
2330
|
-
if (!
|
|
2329
|
+
const parsedReport = this.#usageProviderResolver?.(provider)?.parseRateLimitHeaders?.(headers, now);
|
|
2330
|
+
if (!parsedReport) return false;
|
|
2331
|
+
const metadata: Record<string, unknown> = { ...(parsedReport.metadata ?? {}) };
|
|
2332
|
+
if (credential.accountId && metadata.accountId === undefined) metadata.accountId = credential.accountId;
|
|
2333
|
+
if (credential.email && metadata.email === undefined) metadata.email = credential.email;
|
|
2334
|
+
if (credential.projectId && metadata.projectId === undefined) metadata.projectId = credential.projectId;
|
|
2335
|
+
const report: UsageReport = { ...parsedReport, metadata };
|
|
2331
2336
|
|
|
2332
2337
|
const prior = this.#usageCache.getStale<UsageReport | null>(cacheKey)?.value;
|
|
2333
2338
|
let merged = report;
|
|
@@ -2351,6 +2356,7 @@ export class AuthStorage {
|
|
|
2351
2356
|
fetchedAt: now,
|
|
2352
2357
|
limits,
|
|
2353
2358
|
metadata: {
|
|
2359
|
+
...(report.metadata ?? {}),
|
|
2354
2360
|
...(prior.metadata ?? {}),
|
|
2355
2361
|
headersUpdatedAt: now,
|
|
2356
2362
|
},
|
|
@@ -120,10 +120,11 @@ export function buildBetaHeader(baseBetas: readonly string[], extraBetas: readon
|
|
|
120
120
|
}
|
|
121
121
|
|
|
122
122
|
const midConversationSystemBeta = "mid-conversation-system-2026-04-07";
|
|
123
|
+
const contextManagementBeta = "context-management-2025-06-27";
|
|
123
124
|
const claudeCodeUtilityBetaDefaults = [
|
|
124
125
|
"oauth-2025-04-20",
|
|
125
126
|
"interleaved-thinking-2025-05-14",
|
|
126
|
-
|
|
127
|
+
contextManagementBeta,
|
|
127
128
|
"prompt-caching-scope-2026-01-05",
|
|
128
129
|
"structured-outputs-2025-12-15",
|
|
129
130
|
] as const;
|
|
@@ -131,7 +132,7 @@ const claudeCodeAgentBetaDefaults = [
|
|
|
131
132
|
"claude-code-20250219",
|
|
132
133
|
"oauth-2025-04-20",
|
|
133
134
|
"interleaved-thinking-2025-05-14",
|
|
134
|
-
|
|
135
|
+
contextManagementBeta,
|
|
135
136
|
"prompt-caching-scope-2026-01-05",
|
|
136
137
|
midConversationSystemBeta,
|
|
137
138
|
"advanced-tool-use-2025-11-20",
|
|
@@ -192,10 +193,18 @@ export function buildAnthropicHeaders(options: AnthropicHeaderOptions): Record<s
|
|
|
192
193
|
const oauthToken = options.isOAuth ?? isAnthropicOAuthToken(options.apiKey);
|
|
193
194
|
const extraBetas = options.extraBetas ?? [];
|
|
194
195
|
const stream = options.stream ?? false;
|
|
195
|
-
// `enforcedHeaderKeys` strips User-Agent
|
|
196
|
-
//
|
|
197
|
-
//
|
|
196
|
+
// `enforcedHeaderKeys` strips User-Agent / X-Api-Key / Authorization out of
|
|
197
|
+
// modelHeaders so a case-insensitive spread can't produce duplicate keys; each
|
|
198
|
+
// branch re-adds the caller's value explicitly. User-Agent and X-Api-Key are
|
|
199
|
+
// always honored (with branch-specific defaults filling in when absent), while
|
|
200
|
+
// Authorization is honored for every non-OAuth, non-Cloudflare-gateway branch —
|
|
201
|
+
// OAuth requests MUST carry `Authorization: Bearer <oauth-token>` (the OAuth
|
|
202
|
+
// credential itself) and Cloudflare AI Gateway authenticates via
|
|
203
|
+
// `cf-aig-authorization`, so user-supplied auth there would just leak. Both of
|
|
204
|
+
// those cases drop + log the caller value (#3391).
|
|
198
205
|
const incomingUserAgent = getHeaderCaseInsensitive(options.modelHeaders, "User-Agent");
|
|
206
|
+
const incomingAuthorization = getHeaderCaseInsensitive(options.modelHeaders, "Authorization");
|
|
207
|
+
const incomingApiKey = getHeaderCaseInsensitive(options.modelHeaders, "X-Api-Key");
|
|
199
208
|
// Claude Code betas (oauth-2025-04-20, claude-code-20250219, …) are part of
|
|
200
209
|
// the OAuth fingerprint; API-key requests default to extras only, matching
|
|
201
210
|
// the streaming path (buildAnthropicClientOptions passes [] for non-OAuth).
|
|
@@ -204,14 +213,21 @@ export function buildAnthropicHeaders(options: AnthropicHeaderOptions): Record<s
|
|
|
204
213
|
extraBetas,
|
|
205
214
|
);
|
|
206
215
|
const acceptHeader = oauthToken ? "application/json" : stream ? "text/event-stream" : "application/json";
|
|
216
|
+
const isCloudflare = options.isCloudflareAiGateway ?? false;
|
|
217
|
+
const honorAuthorization = !oauthToken && !isCloudflare;
|
|
218
|
+
const honorApiKey = !isCloudflare;
|
|
207
219
|
const modelHeaders: Record<string, string> = {};
|
|
208
220
|
const filteredEnforcedKeys: string[] = [];
|
|
209
221
|
for (const [key, value] of Object.entries(options.modelHeaders ?? {})) {
|
|
210
222
|
const lowerKey = key.toLowerCase();
|
|
211
223
|
if (enforcedHeaderKeys.has(lowerKey)) {
|
|
212
|
-
//
|
|
213
|
-
//
|
|
214
|
-
|
|
224
|
+
// user-agent is always re-applied explicitly. authorization / x-api-key
|
|
225
|
+
// are silently re-applied in honoring branches and dropped + logged
|
|
226
|
+
// where the branch enforces its own credential.
|
|
227
|
+
if (lowerKey === "user-agent") continue;
|
|
228
|
+
if (lowerKey === "authorization" && honorAuthorization) continue;
|
|
229
|
+
if (lowerKey === "x-api-key" && honorApiKey) continue;
|
|
230
|
+
filteredEnforcedKeys.push(key);
|
|
215
231
|
continue;
|
|
216
232
|
}
|
|
217
233
|
modelHeaders[key] = value;
|
|
@@ -225,7 +241,7 @@ export function buildAnthropicHeaders(options: AnthropicHeaderOptions): Record<s
|
|
|
225
241
|
});
|
|
226
242
|
}
|
|
227
243
|
|
|
228
|
-
if (
|
|
244
|
+
if (isCloudflare) {
|
|
229
245
|
return {
|
|
230
246
|
...modelHeaders,
|
|
231
247
|
Accept: acceptHeader,
|
|
@@ -250,15 +266,17 @@ export function buildAnthropicHeaders(options: AnthropicHeaderOptions): Record<s
|
|
|
250
266
|
...(options.claudeCodeSessionId ? { "X-Claude-Code-Session-Id": options.claudeCodeSessionId } : {}),
|
|
251
267
|
"x-client-request-id": nodeCrypto.randomUUID(),
|
|
252
268
|
"User-Agent": userAgent,
|
|
269
|
+
...(incomingApiKey ? { "X-Api-Key": incomingApiKey } : {}),
|
|
253
270
|
};
|
|
254
271
|
} else if (!isOfficialAnthropicApiUrl(options.baseUrl)) {
|
|
255
272
|
return {
|
|
256
273
|
...modelHeaders,
|
|
257
274
|
Accept: acceptHeader,
|
|
258
|
-
Authorization: `Bearer ${options.apiKey}`,
|
|
275
|
+
Authorization: incomingAuthorization ?? `Bearer ${options.apiKey}`,
|
|
259
276
|
...sharedHeaders,
|
|
260
277
|
...(incomingUserAgent ? { "User-Agent": incomingUserAgent } : {}),
|
|
261
278
|
...(betaHeader ? { "anthropic-beta": betaHeader } : {}),
|
|
279
|
+
...(incomingApiKey ? { "X-Api-Key": incomingApiKey } : {}),
|
|
262
280
|
};
|
|
263
281
|
} else {
|
|
264
282
|
return {
|
|
@@ -267,7 +285,8 @@ export function buildAnthropicHeaders(options: AnthropicHeaderOptions): Record<s
|
|
|
267
285
|
...sharedHeaders,
|
|
268
286
|
...(incomingUserAgent ? { "User-Agent": incomingUserAgent } : {}),
|
|
269
287
|
...(betaHeader ? { "anthropic-beta": betaHeader } : {}),
|
|
270
|
-
|
|
288
|
+
...(incomingAuthorization ? { Authorization: incomingAuthorization } : {}),
|
|
289
|
+
"X-Api-Key": incomingApiKey ?? options.apiKey,
|
|
271
290
|
};
|
|
272
291
|
}
|
|
273
292
|
}
|
|
@@ -1680,6 +1699,22 @@ const streamAnthropicOnce = (
|
|
|
1680
1699
|
// carry it in the Claude Code list).
|
|
1681
1700
|
extraBetas.push(midConversationSystemBeta);
|
|
1682
1701
|
}
|
|
1702
|
+
// `context_management.clear_thinking_20251015` requires this beta. OAuth
|
|
1703
|
+
// requests carry it in `claudeCodeAgentBetaDefaults`; API-key requests
|
|
1704
|
+
// need it added explicitly so the field is honored instead of rejected
|
|
1705
|
+
// (#3288). Skip transports where this package cannot deliver the beta
|
|
1706
|
+
// in the form their adapter accepts: Copilot strips Anthropic betas,
|
|
1707
|
+
// and Vertex rawPredict needs betas in the body (`anthropic_beta`),
|
|
1708
|
+
// not as an `anthropic-beta` HTTP header.
|
|
1709
|
+
if (
|
|
1710
|
+
model.reasoning &&
|
|
1711
|
+
options?.thinkingEnabled &&
|
|
1712
|
+
model.provider !== "github-copilot" &&
|
|
1713
|
+
model.provider !== "google-vertex" &&
|
|
1714
|
+
!extraBetas.includes(contextManagementBeta)
|
|
1715
|
+
) {
|
|
1716
|
+
extraBetas.push(contextManagementBeta);
|
|
1717
|
+
}
|
|
1683
1718
|
|
|
1684
1719
|
const created = createClient(model, {
|
|
1685
1720
|
model,
|
|
@@ -2530,12 +2565,15 @@ export function buildAnthropicClientOptions(args: AnthropicClientOptionsArgs): A
|
|
|
2530
2565
|
};
|
|
2531
2566
|
}
|
|
2532
2567
|
|
|
2568
|
+
// Suppress the client-level `X-Api-Key` whenever an `Authorization` header
|
|
2569
|
+
// already sits in `defaultHeaders` for a non-official, non-OAuth endpoint —
|
|
2570
|
+
// either our auto-built `Bearer <apiKey>` or a caller-supplied custom auth
|
|
2571
|
+
// scheme via `model.headers` (#3391). Adding a bonus `X-Api-Key` would force
|
|
2572
|
+
// the proxy to deal with two competing credentials when the user explicitly
|
|
2573
|
+
// asked for one.
|
|
2533
2574
|
const authorizationHeader = getHeaderCaseInsensitive(defaultHeaders, "Authorization");
|
|
2534
2575
|
const shouldSuppressClientApiKey =
|
|
2535
|
-
!oauthToken &&
|
|
2536
|
-
!model.compat.officialEndpoint &&
|
|
2537
|
-
typeof authorizationHeader === "string" &&
|
|
2538
|
-
/^Bearer\s+/i.test(authorizationHeader);
|
|
2576
|
+
!oauthToken && !model.compat.officialEndpoint && typeof authorizationHeader === "string";
|
|
2539
2577
|
|
|
2540
2578
|
return {
|
|
2541
2579
|
isOAuthToken: oauthToken,
|
|
@@ -2944,11 +2982,28 @@ function buildParams(
|
|
|
2944
2982
|
}
|
|
2945
2983
|
}
|
|
2946
2984
|
|
|
2947
|
-
// Pre-compute context_management
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2985
|
+
// Pre-compute context_management. Send keep: "all" for every enabled or
|
|
2986
|
+
// adaptive thinking request (OAuth + API-key) — not just OAuth. Without
|
|
2987
|
+
// this directive Anthropic-compatible backends (Z.AI, Kimi, DeepSeek, …)
|
|
2988
|
+
// strip the replayed thinking blocks `replayUnsignedThinking` puts back
|
|
2989
|
+
// on the wire, so the model loses the prior reasoning chain across turns
|
|
2990
|
+
// and the KV cache misses every turn (#3288). Narrowing this guard back
|
|
2991
|
+
// to `isOAuthToken` regresses every API-key thinking provider. Skip
|
|
2992
|
+
// injected clients because this code cannot add the required
|
|
2993
|
+
// `context-management-2025-06-27` beta to caller-owned SDK clients. Skip
|
|
2994
|
+
// Copilot because its proxy strips Anthropic betas and demotes thinking
|
|
2995
|
+
// blocks to text upstream, so `keep: "all"` is a no-op that risks proxy
|
|
2996
|
+
// rejection of an unrecognized field. Skip Vertex rawPredict because that
|
|
2997
|
+
// adapter requires betas in the JSON body (`anthropic_beta`) instead of the
|
|
2998
|
+
// Anthropic HTTP beta header this code can add.
|
|
2999
|
+
const shouldKeepThinkingContext =
|
|
3000
|
+
!options?.client &&
|
|
3001
|
+
model.provider !== "github-copilot" &&
|
|
3002
|
+
model.provider !== "google-vertex" &&
|
|
3003
|
+
(thinking?.type === "adaptive" || thinking?.type === "enabled");
|
|
3004
|
+
const contextManagement = shouldKeepThinkingContext
|
|
3005
|
+
? { edits: [{ type: "clear_thinking_20251015" as const, keep: "all" as const }] }
|
|
3006
|
+
: undefined;
|
|
2952
3007
|
|
|
2953
3008
|
// Pre-compute output_config.
|
|
2954
3009
|
const outputConfigEntries: AnthropicOutputConfig = {};
|
package/src/providers/ollama.ts
CHANGED
|
@@ -282,6 +282,26 @@ function convertTools(tools: Tool[] | undefined): OllamaFunctionTool[] | undefin
|
|
|
282
282
|
}));
|
|
283
283
|
}
|
|
284
284
|
|
|
285
|
+
/**
|
|
286
|
+
* Ollama Cloud rejects `num_predict` above this value with HTTP 400
|
|
287
|
+
* (`max_tokens (...) exceeds model's maximum output tokens (65536)`).
|
|
288
|
+
* The cap currently applies uniformly to cloud-served models; the cloud-side
|
|
289
|
+
* limit was confirmed empirically against `deepseek-v4-pro`/`-flash` and is
|
|
290
|
+
* the same cap surfaced for every other Ollama Cloud model we've probed.
|
|
291
|
+
*
|
|
292
|
+
* Acts as a wire-level safety net so stale `models.db` rows (or custom
|
|
293
|
+
* `modelOverrides` re-enabling `num_predict`) cannot 400 the request — even
|
|
294
|
+
* when `model.omitMaxOutputTokens` was never applied. See #3392.
|
|
295
|
+
*/
|
|
296
|
+
const OLLAMA_CLOUD_NUM_PREDICT_CAP = 65_536;
|
|
297
|
+
|
|
298
|
+
function resolveNumPredict(model: Model<"ollama-chat">, requested: number): number {
|
|
299
|
+
if (model.provider === "ollama-cloud") {
|
|
300
|
+
return Math.min(requested, OLLAMA_CLOUD_NUM_PREDICT_CAP);
|
|
301
|
+
}
|
|
302
|
+
return requested;
|
|
303
|
+
}
|
|
304
|
+
|
|
285
305
|
function createChatBody(model: Model<"ollama-chat">, context: Context, options: OllamaChatOptions | undefined) {
|
|
286
306
|
const think = mapReasoning(model, options?.reasoning, options?.disableReasoning);
|
|
287
307
|
const toolChoice = mapToolChoice(options?.toolChoice);
|
|
@@ -294,7 +314,7 @@ function createChatBody(model: Model<"ollama-chat">, context: Context, options:
|
|
|
294
314
|
...(think !== undefined ? { think } : {}),
|
|
295
315
|
...(toolChoice !== undefined ? { tool_choice: toolChoice } : {}),
|
|
296
316
|
...(options?.maxTokens !== undefined && !model.omitMaxOutputTokens
|
|
297
|
-
? { options: { num_predict: options.maxTokens } }
|
|
317
|
+
? { options: { num_predict: resolveNumPredict(model, options.maxTokens) } }
|
|
298
318
|
: {}),
|
|
299
319
|
stream: true,
|
|
300
320
|
};
|
|
@@ -3,6 +3,7 @@ import { $flag, extractHttpStatusFromError, logger, structuredCloneJSON } from "
|
|
|
3
3
|
import { getEnvApiKey } from "../stream";
|
|
4
4
|
import type {
|
|
5
5
|
AssistantMessage,
|
|
6
|
+
CacheRetention,
|
|
6
7
|
Context,
|
|
7
8
|
Model,
|
|
8
9
|
OpenAICompat,
|
|
@@ -324,6 +325,8 @@ function markOpenAIResponsesChainZeroDataRetention(chain: OpenAIResponsesChainSt
|
|
|
324
325
|
});
|
|
325
326
|
}
|
|
326
327
|
|
|
328
|
+
type OpenRouterAnthropicCacheControl = { type: "ephemeral"; ttl?: "1h" };
|
|
329
|
+
|
|
327
330
|
type OpenAIResponsesSamplingParams = ResponseCreateParamsStreaming & {
|
|
328
331
|
top_p?: number;
|
|
329
332
|
top_k?: number;
|
|
@@ -334,8 +337,19 @@ type OpenAIResponsesSamplingParams = ResponseCreateParamsStreaming & {
|
|
|
334
337
|
stream_options?: { include_obfuscation?: boolean };
|
|
335
338
|
provider?: OpenAICompat["openRouterRouting"];
|
|
336
339
|
reasoning?: { effort?: string } | { enabled: false };
|
|
340
|
+
cache_control?: OpenRouterAnthropicCacheControl;
|
|
337
341
|
};
|
|
338
342
|
|
|
343
|
+
function maybeAddOpenRouterAnthropicCacheControl(
|
|
344
|
+
params: OpenAIResponsesSamplingParams,
|
|
345
|
+
model: Model<"openai-responses">,
|
|
346
|
+
cacheRetention: CacheRetention,
|
|
347
|
+
): void {
|
|
348
|
+
if (cacheRetention === "none" || !isOpenRouterAnthropicModel(model)) return;
|
|
349
|
+
if (params.cache_control != null) return;
|
|
350
|
+
params.cache_control = cacheRetention === "long" ? { type: "ephemeral", ttl: "1h" } : { type: "ephemeral" };
|
|
351
|
+
}
|
|
352
|
+
|
|
339
353
|
/**
|
|
340
354
|
* Generate function for OpenAI Responses API
|
|
341
355
|
*/
|
|
@@ -777,7 +791,7 @@ export function buildParams(
|
|
|
777
791
|
replay: shouldReplayNativeHistory,
|
|
778
792
|
filterReasoning: policy.reasoning.filterReasoningHistory,
|
|
779
793
|
},
|
|
780
|
-
includeThinkingSignatures: shouldReplayNativeHistory,
|
|
794
|
+
includeThinkingSignatures: shouldReplayNativeHistory && !policy.reasoning.filterReasoningHistory,
|
|
781
795
|
repairOrphanOutputs: true,
|
|
782
796
|
});
|
|
783
797
|
|
|
@@ -823,6 +837,7 @@ export function buildParams(
|
|
|
823
837
|
store: false,
|
|
824
838
|
stream_options: model.compat.supportsObfuscationOptOut ? { include_obfuscation: false } : undefined,
|
|
825
839
|
};
|
|
840
|
+
maybeAddOpenRouterAnthropicCacheControl(params, model, cacheRetention);
|
|
826
841
|
const outputToken = resolveOpenAIOutputTokenParam({
|
|
827
842
|
field: "max_output_tokens",
|
|
828
843
|
maxTokens: options?.maxTokens,
|
package/src/stream.ts
CHANGED
|
@@ -847,10 +847,15 @@ function mapOptionsForApi<TApi extends Api>(
|
|
|
847
847
|
});
|
|
848
848
|
}
|
|
849
849
|
|
|
850
|
+
const thinkingMode = model.thinking?.mode;
|
|
851
|
+
const effort =
|
|
852
|
+
thinkingMode === "anthropic-adaptive" || thinkingMode === "anthropic-budget-effort"
|
|
853
|
+
? mapEffortToAnthropicAdaptiveEffort(model, reasoning)
|
|
854
|
+
: undefined;
|
|
855
|
+
|
|
850
856
|
// For Opus 4.6+ and Sonnet 4.6+: use adaptive thinking with effort level
|
|
851
857
|
// For older models: use budget-based thinking
|
|
852
|
-
if (
|
|
853
|
-
const effort = mapEffortToAnthropicAdaptiveEffort(model, reasoning);
|
|
858
|
+
if (thinkingMode === "anthropic-adaptive") {
|
|
854
859
|
return castApi<"anthropic-messages">({
|
|
855
860
|
...base,
|
|
856
861
|
requestModelId: resolveWireModelId(model, reasoning),
|
|
@@ -868,6 +873,7 @@ function mapOptionsForApi<TApi extends Api>(
|
|
|
868
873
|
requestModelId: resolveWireModelId(model, reasoning),
|
|
869
874
|
thinkingEnabled: true,
|
|
870
875
|
thinkingBudgetTokens: thinkingBudget,
|
|
876
|
+
effort,
|
|
871
877
|
toolChoice: mapAnthropicToolChoice(options?.toolChoice),
|
|
872
878
|
thinkingDisplay: options?.hideThinkingSummary ? "omitted" : undefined,
|
|
873
879
|
serviceTier: options?.serviceTier,
|
|
@@ -899,6 +905,7 @@ function mapOptionsForApi<TApi extends Api>(
|
|
|
899
905
|
requestModelId: resolveWireModelId(model, reasoning),
|
|
900
906
|
thinkingEnabled: true,
|
|
901
907
|
thinkingBudgetTokens: thinkingBudget,
|
|
908
|
+
effort,
|
|
902
909
|
toolChoice: mapAnthropicToolChoice(options?.toolChoice),
|
|
903
910
|
thinkingDisplay: options?.hideThinkingSummary ? "omitted" : undefined,
|
|
904
911
|
serviceTier: options?.serviceTier,
|
package/src/usage/opencode-go.ts
CHANGED
|
@@ -62,7 +62,6 @@ function buildWindowLimit(
|
|
|
62
62
|
unit: "usd",
|
|
63
63
|
},
|
|
64
64
|
status: resolveStatus(usedFraction),
|
|
65
|
-
notes: ["OMP-observed spend only; OpenCode usage outside OMP is not included."],
|
|
66
65
|
};
|
|
67
66
|
}
|
|
68
67
|
|
|
@@ -80,6 +79,7 @@ export const opencodeGoUsageProvider: UsageProvider = {
|
|
|
80
79
|
provider: OPENCODE_GO_PROVIDER,
|
|
81
80
|
fetchedAt: nowMs,
|
|
82
81
|
limits: OPENCODE_GO_LIMITS.map(limit => buildWindowLimit(limit, entries, nowMs)),
|
|
82
|
+
notes: ["OMP-observed spend only; OpenCode usage outside OMP is not included."],
|
|
83
83
|
metadata: {
|
|
84
84
|
planType: "OpenCode Go",
|
|
85
85
|
source: "omp-observed-request-costs",
|
package/src/usage.ts
CHANGED
|
@@ -82,6 +82,13 @@ export interface UsageReport {
|
|
|
82
82
|
limits: UsageLimit[];
|
|
83
83
|
/** Saved rate-limit resets the account can redeem, when the provider reports them. */
|
|
84
84
|
resetCredits?: UsageResetCredits;
|
|
85
|
+
/**
|
|
86
|
+
* Provider-wide disclaimers shown once above per-account sections.
|
|
87
|
+
* Use this for caveats that apply to every limit (e.g. "OMP-observed
|
|
88
|
+
* spend only"). Per-limit notes that differ per window (e.g. "Overage
|
|
89
|
+
* requests: N") stay on {@link UsageLimit.notes}.
|
|
90
|
+
*/
|
|
91
|
+
notes?: string[];
|
|
85
92
|
metadata?: Record<string, unknown>;
|
|
86
93
|
raw?: unknown;
|
|
87
94
|
}
|
|
@@ -204,6 +211,7 @@ export const usageReportSchema = type({
|
|
|
204
211
|
fetchedAt: "number",
|
|
205
212
|
limits: usageLimitSchema.array(),
|
|
206
213
|
"resetCredits?": usageResetCreditsSchema,
|
|
214
|
+
"notes?": "string[]",
|
|
207
215
|
"metadata?": { "[string]": "unknown" },
|
|
208
216
|
// `raw` is provider-specific and may be anything; the broker strips it before
|
|
209
217
|
// sending the report over the wire, so accept-but-ignore here.
|
package/src/utils.ts
CHANGED
|
@@ -78,6 +78,7 @@ function sanitizeOpenAIResponsesHistoryItemForReplay(
|
|
|
78
78
|
): OpenAIResponsesReplayItem | undefined {
|
|
79
79
|
if (item.type === "item_reference") return undefined;
|
|
80
80
|
if (item.type === "image_generation_call") return sanitizeOpenAIResponsesImageGenerationCallForReplay(item);
|
|
81
|
+
if (item.type === "reasoning") return sanitizeOpenAIResponsesReasoningItemForReplay(item);
|
|
81
82
|
|
|
82
83
|
// providerPayload stores raw output items; replay strips item ids and keeps only normalized call_id.
|
|
83
84
|
const { id: _id, ...sanitizedItem } = item;
|
|
@@ -88,6 +89,19 @@ function sanitizeOpenAIResponsesHistoryItemForReplay(
|
|
|
88
89
|
return sanitizedItem as unknown as OpenAIResponsesReplayItem;
|
|
89
90
|
}
|
|
90
91
|
|
|
92
|
+
function sanitizeOpenAIResponsesReasoningItemForReplay(item: Record<string, unknown>): OpenAIResponsesReplayItem {
|
|
93
|
+
const sanitizedItem: Record<string, unknown> = { type: "reasoning" };
|
|
94
|
+
if (Array.isArray(item.summary)) sanitizedItem.summary = item.summary;
|
|
95
|
+
if (Array.isArray(item.content)) sanitizedItem.content = item.content;
|
|
96
|
+
if (typeof item.encrypted_content === "string" || item.encrypted_content === null) {
|
|
97
|
+
sanitizedItem.encrypted_content = item.encrypted_content;
|
|
98
|
+
}
|
|
99
|
+
if (item.status === "in_progress" || item.status === "completed" || item.status === "incomplete") {
|
|
100
|
+
sanitizedItem.status = item.status;
|
|
101
|
+
}
|
|
102
|
+
return sanitizedItem as unknown as OpenAIResponsesReplayItem;
|
|
103
|
+
}
|
|
104
|
+
|
|
91
105
|
function sanitizeOpenAIResponsesImageGenerationCallForReplay(
|
|
92
106
|
item: Record<string, unknown>,
|
|
93
107
|
): ResponseInputItem.ImageGenerationCall | undefined {
|