




















1+import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce";
2+import type { OpenClawConfig } from "../config/types.openclaw.js";
3+import {
4+lookupCachedContextTokens,
5+lookupCachedContextWindow,
6+minPositiveContextTokens,
7+providerContextTokenCacheKey,
8+} from "./context-cache.js";
9+import { normalizeProviderId } from "./model-selection.js";
10+11+type ConfigModelEntry = { id?: string; contextWindow?: number; contextTokens?: number };
12+type ProviderConfigEntry = {
13+contextWindow?: number;
14+contextTokens?: number;
15+models?: ConfigModelEntry[];
16+};
17+export type ModelsConfig = {
18+providers?: Record<string, ProviderConfigEntry | undefined>;
19+};
20+21+export type ContextTokenResolutionParams = {
22+cfg?: OpenClawConfig;
23+sourceCfg?: OpenClawConfig | null;
24+provider?: string;
25+model?: string;
26+contextTokensOverride?: number;
27+fallbackContextTokens?: number;
28+modelContextWindow?: number;
29+modelContextTokens?: number;
30+allowAsyncLoad?: boolean;
31+};
32+33+const ANTHROPIC_GA_1M_MODEL_PREFIXES = [
34+"claude-opus-4-8",
35+"claude-opus-4.8",
36+"claude-opus-4-6",
37+"claude-opus-4.6",
38+"claude-opus-4-7",
39+"claude-opus-4.7",
40+"claude-sonnet-4-6",
41+"claude-sonnet-4.6",
42+] as const;
43+export const ANTHROPIC_CONTEXT_1M_TOKENS = 1_048_576;
44+export const ANTHROPIC_VERTEX_CONTEXT_1M_TOKENS = 1_000_000;
45+export const ANTHROPIC_FABLE_CONTEXT_TOKENS = 1_000_000;
46+47+type ConfiguredContextTokens = {
48+value: number;
49+source: "contextTokens" | "contextWindow";
50+};
51+52+function resolveProviderModelRef(params: {
53+provider?: string;
54+model?: string;
55+}): { provider: string; model: string } | undefined {
56+const modelRaw = params.model?.trim();
57+if (!modelRaw) {
58+return undefined;
59+}
60+const providerRaw = params.provider?.trim();
61+if (providerRaw) {
62+const provider = normalizeProviderId(providerRaw);
63+return provider ? { provider, model: modelRaw } : undefined;
64+}
65+const slash = modelRaw.indexOf("/");
66+if (slash <= 0) {
67+return undefined;
68+}
69+const provider = normalizeProviderId(modelRaw.slice(0, slash));
70+const model = modelRaw.slice(slash + 1).trim();
71+return provider && model ? { provider, model } : undefined;
72+}
73+74+function resolveConfiguredProviderContextTokens(
75+cfg: OpenClawConfig | null | undefined,
76+provider: string,
77+model: string,
78+): ConfiguredContextTokens | undefined {
79+const providers = (cfg?.models as ModelsConfig | undefined)?.providers;
80+if (!providers) {
81+return undefined;
82+}
83+84+function readProviderContextTokens(
85+providerConfig: ProviderConfigEntry | undefined,
86+): ConfiguredContextTokens | undefined {
87+if (typeof providerConfig?.contextTokens === "number" && providerConfig.contextTokens > 0) {
88+return { value: providerConfig.contextTokens, source: "contextTokens" };
89+}
90+if (typeof providerConfig?.contextWindow === "number" && providerConfig.contextWindow > 0) {
91+return { value: providerConfig.contextWindow, source: "contextWindow" };
92+}
93+return undefined;
94+}
95+96+function findContextTokens(
97+matchProviderId: (id: string) => boolean,
98+): ConfiguredContextTokens | undefined {
99+for (const [providerId, providerConfig] of Object.entries(providers!)) {
100+if (!matchProviderId(providerId)) {
101+continue;
102+}
103+if (Array.isArray(providerConfig?.models)) {
104+for (const entry of providerConfig.models) {
105+const entryId = typeof entry?.id === "string" ? entry.id : "";
106+const slash = entryId.indexOf("/");
107+const prefixedProvider = slash > 0 ? normalizeProviderId(entryId.slice(0, slash)) : "";
108+const bareEntryId = slash > 0 ? entryId.slice(slash + 1).trim() : "";
109+const modelMatches =
110+entryId === model ||
111+(prefixedProvider === normalizeProviderId(providerId) && bareEntryId === model);
112+if (modelMatches && typeof entry.contextTokens === "number" && entry.contextTokens > 0) {
113+return { value: entry.contextTokens, source: "contextTokens" };
114+}
115+if (modelMatches && typeof entry.contextWindow === "number" && entry.contextWindow > 0) {
116+return { value: entry.contextWindow, source: "contextWindow" };
117+}
118+}
119+}
120+const providerContextTokens = readProviderContextTokens(providerConfig);
121+if (providerContextTokens) {
122+return providerContextTokens;
123+}
124+}
125+return undefined;
126+}
127+128+// Match exact config keys before normalized aliases so one provider cannot
129+// inherit another provider's context cap based on object iteration order.
130+const exactResult = findContextTokens(
131+(id) => normalizeLowercaseStringOrEmpty(id) === normalizeLowercaseStringOrEmpty(provider),
132+);
133+if (exactResult !== undefined) {
134+return exactResult;
135+}
136+const normalizedProvider = normalizeProviderId(provider);
137+return findContextTokens((id) => normalizeProviderId(id) === normalizedProvider);
138+}
139+140+function resolveModelFamilyId(modelId: string): string {
141+const normalized = normalizeLowercaseStringOrEmpty(modelId);
142+return normalized.includes("/") ? (normalized.split("/").at(-1) ?? normalized) : normalized;
143+}
144+145+export function resolveAnthropicFixedContextWindow(
146+provider: string,
147+model: string,
148+): number | undefined {
149+const modelId = resolveModelFamilyId(model);
150+if (
151+(provider === "anthropic" || provider === "anthropic-vertex") &&
152+modelId.startsWith("claude-fable-5")
153+) {
154+return ANTHROPIC_FABLE_CONTEXT_TOKENS;
155+}
156+if (provider !== "anthropic" && provider !== "anthropic-vertex" && provider !== "claude-cli") {
157+return undefined;
158+}
159+if (!ANTHROPIC_GA_1M_MODEL_PREFIXES.some((prefix) => modelId.startsWith(prefix))) {
160+return undefined;
161+}
162+return provider === "anthropic-vertex"
163+ ? ANTHROPIC_VERTEX_CONTEXT_1M_TOKENS
164+ : ANTHROPIC_CONTEXT_1M_TOKENS;
165+}
166+167+export function resolveContextTokensForModelFromCache(
168+params: ContextTokenResolutionParams,
169+lookupContextTokens: (modelId?: string) => number | undefined = lookupCachedContextTokens,
170+lookupContextWindow: (modelId?: string) => number | undefined = lookupCachedContextWindow,
171+): number | undefined {
172+const ref = resolveProviderModelRef(params);
173+const override =
174+typeof params.contextTokensOverride === "number" && params.contextTokensOverride > 0
175+ ? params.contextTokensOverride
176+ : undefined;
177+const capOverride = (contextTokens: number) =>
178+override !== undefined ? Math.min(override, contextTokens) : contextTokens;
179+const explicitProvider = params.provider?.trim();
180+181+if (ref && explicitProvider) {
182+const configuredWindow = resolveConfiguredProviderContextTokens(
183+params.cfg,
184+explicitProvider,
185+ref.model,
186+);
187+const sourceConfig = params.sourceCfg === undefined ? params.cfg : params.sourceCfg;
188+const sourceConfiguredWindow = resolveConfiguredProviderContextTokens(
189+sourceConfig,
190+explicitProvider,
191+ref.model,
192+);
193+const fixedContextWindow = resolveAnthropicFixedContextWindow(ref.provider, ref.model);
194+const providerResult = lookupContextTokens(
195+providerContextTokenCacheKey(normalizeProviderId(ref.provider), ref.model),
196+);
197+const providerWindow = lookupContextWindow(
198+providerContextTokenCacheKey(normalizeProviderId(ref.provider), ref.model),
199+);
200+const modelContextTokens =
201+typeof params.modelContextTokens === "number" && params.modelContextTokens > 0
202+ ? params.modelContextTokens
203+ : undefined;
204+const modelContextWindow =
205+typeof params.modelContextWindow === "number" && params.modelContextWindow > 0
206+ ? params.modelContextWindow
207+ : undefined;
208+const runtimeCap = minPositiveContextTokens(
209+providerResult,
210+modelContextTokens,
211+fixedContextWindow === undefined ? providerWindow : undefined,
212+fixedContextWindow === undefined ? modelContextWindow : undefined,
213+);
214+if (configuredWindow) {
215+if (configuredWindow.source === "contextTokens") {
216+const effectiveCap =
217+fixedContextWindow === undefined
218+ ? configuredWindow.value
219+ : Math.min(configuredWindow.value, fixedContextWindow);
220+return capOverride(effectiveCap);
221+}
222+const authoredContextWindow =
223+sourceConfiguredWindow?.source === "contextWindow"
224+ ? sourceConfiguredWindow.value
225+ : undefined;
226+// Runtime config fills omitted contextWindow values with 200k. Only an
227+// authored window may lower a fixed provider contract; contextTokens is
228+// always an explicit effective-cap override above.
229+if (fixedContextWindow !== undefined && authoredContextWindow === undefined) {
230+const effectiveCap =
231+runtimeCap === undefined ? fixedContextWindow : Math.min(runtimeCap, fixedContextWindow);
232+return capOverride(effectiveCap);
233+}
234+if (fixedContextWindow !== undefined) {
235+const effectiveCap = minPositiveContextTokens(
236+authoredContextWindow,
237+fixedContextWindow,
238+runtimeCap,
239+);
240+return effectiveCap === undefined ? undefined : capOverride(effectiveCap);
241+}
242+if (runtimeCap !== undefined) {
243+return capOverride(Math.min(configuredWindow.value, runtimeCap));
244+}
245+return capOverride(configuredWindow.value);
246+}
247+if (runtimeCap !== undefined) {
248+const effectiveCap =
249+fixedContextWindow === undefined ? runtimeCap : Math.min(runtimeCap, fixedContextWindow);
250+return capOverride(effectiveCap);
251+}
252+if (fixedContextWindow !== undefined) {
253+return capOverride(fixedContextWindow);
254+}
255+}
256+257+// Model-only calls use the raw discovery key. With an explicit provider,
258+// slash-containing raw keys lack ownership provenance and cannot lower an override.
259+const bareResult = lookupContextTokens(params.model);
260+const bareWindow = lookupContextWindow(params.model);
261+const bareCap = minPositiveContextTokens(bareResult, bareWindow);
262+if (bareCap !== undefined) {
263+const ambiguousSlashId = Boolean(explicitProvider && ref?.model.includes("/"));
264+return ambiguousSlashId && override !== undefined ? override : capOverride(bareCap);
265+}
266+267+return override ?? params.fallbackContextTokens;
268+}
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。