@oh-my-pi/pi-ai 18.2.5 → 18.2.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 +20 -1
- package/THIRD-PARTY-NOTICES.txt +0 -37
- package/dist/types/auth-retry.d.ts +2 -0
- package/dist/types/auth-storage.d.ts +2 -2
- package/dist/types/error/classes.d.ts +4 -1
- package/dist/types/error/flags.d.ts +12 -4
- package/dist/types/index.d.ts +14 -12
- package/dist/types/judgment/types.d.ts +5 -2
- package/dist/types/judgment/typesafe.d.ts +16 -22
- package/dist/types/providers/anthropic-compaction.d.ts +10 -0
- package/dist/types/providers/anthropic-identity.d.ts +18 -0
- package/dist/types/providers/anthropic-state.d.ts +13 -0
- package/dist/types/providers/anthropic.d.ts +6 -59
- package/dist/types/providers/gitlab-duo.d.ts +0 -1
- package/dist/types/providers/kimi.d.ts +1 -5
- package/dist/types/providers/openai-codex-attestation.d.ts +6 -0
- package/dist/types/providers/openai-codex-compaction.d.ts +6 -0
- package/dist/types/providers/openai-codex-responses.d.ts +3 -27
- package/dist/types/providers/openai-codex-transport.d.ts +7 -0
- package/dist/types/providers/openai-shared.d.ts +0 -9
- package/dist/types/providers/register-builtins.d.ts +20 -22
- package/dist/types/providers/synthetic.d.ts +1 -5
- package/package.json +6 -6
- package/src/auth-broker/discover.ts +5 -3
- package/src/auth-retry.ts +3 -0
- package/src/auth-storage.ts +28 -22
- package/src/error/auth-classify.ts +10 -2
- package/src/error/classes.ts +23 -3
- package/src/error/flags.ts +51 -16
- package/src/index.ts +15 -12
- package/src/judgment/types.ts +6 -3
- package/src/judgment/typesafe.ts +53 -48
- package/src/provider-session-state.ts +1 -1
- package/src/providers/anthropic-compaction.ts +54 -0
- package/src/providers/anthropic-identity.ts +136 -0
- package/src/providers/anthropic-state.ts +55 -0
- package/src/providers/anthropic.ts +157 -326
- package/src/providers/bedrock-mantle.ts +1 -1
- package/src/providers/gitlab-duo.ts +0 -4
- package/src/providers/kimi.ts +1 -8
- package/src/providers/openai-codex-attestation.ts +19 -0
- package/src/providers/openai-codex-compaction.ts +18 -0
- package/src/providers/openai-codex-responses.ts +9 -68
- package/src/providers/openai-codex-transport.ts +18 -0
- package/src/providers/openai-shared.ts +1 -10
- package/src/providers/register-builtins.ts +113 -320
- package/src/providers/synthetic.ts +1 -8
- package/src/registry/cloudflare-ai-gateway.ts +1 -1
- package/src/stream.ts +7 -15
- package/src/utils/anthropic-auth.ts +3 -6
|
@@ -10,7 +10,6 @@ import { isAnthropicOAuthToken } from "@oh-my-pi/pi-catalog/utils";
|
|
|
10
10
|
import { parseGitHubCopilotApiKey } from "@oh-my-pi/pi-catalog/wire/github-copilot";
|
|
11
11
|
import {
|
|
12
12
|
$env,
|
|
13
|
-
getInstallId,
|
|
14
13
|
isEnoent,
|
|
15
14
|
logger,
|
|
16
15
|
parseJsonWithRepair,
|
|
@@ -129,6 +128,43 @@ import { getOpenAIPromptCacheKey } from "./openai-shared";
|
|
|
129
128
|
import { applyInferenceHeaders } from "./inference-headers";
|
|
130
129
|
import { redactSensitiveCredentials, transformMessages } from "./transform-messages";
|
|
131
130
|
import { NON_VISION_IMAGE_PLACEHOLDER } from "./vision-guard";
|
|
131
|
+
import {
|
|
132
|
+
injectedClientBaseUrl,
|
|
133
|
+
resolvesToOfficialAnthropicEndpoint,
|
|
134
|
+
supportsAnthropicCompaction,
|
|
135
|
+
supportsAnthropicCompactionOnClient,
|
|
136
|
+
} from "./anthropic-compaction";
|
|
137
|
+
import {
|
|
138
|
+
applyClaudeToolPrefix,
|
|
139
|
+
deriveClaudeDeviceId,
|
|
140
|
+
extractClaudeMetadataSessionId,
|
|
141
|
+
generateClaudeCloakingUserId,
|
|
142
|
+
isClaudeCloakingUserId,
|
|
143
|
+
readAnthropicMetadataAccountId,
|
|
144
|
+
readAnthropicMetadataString,
|
|
145
|
+
resolveAnthropicMetadataUserId,
|
|
146
|
+
stripClaudeToolPrefix,
|
|
147
|
+
} from "./anthropic-identity";
|
|
148
|
+
import {
|
|
149
|
+
anthropicProviderSessionStateKey,
|
|
150
|
+
clearAnthropicFastModeFallback,
|
|
151
|
+
isAnthropicFastModeFallbackDisabled,
|
|
152
|
+
normalizeAnthropicBaseUrl,
|
|
153
|
+
resolveDirectAnthropicBaseUrl,
|
|
154
|
+
} from "./anthropic-state";
|
|
155
|
+
|
|
156
|
+
export {
|
|
157
|
+
applyClaudeToolPrefix,
|
|
158
|
+
clearAnthropicFastModeFallback,
|
|
159
|
+
deriveClaudeDeviceId,
|
|
160
|
+
generateClaudeCloakingUserId,
|
|
161
|
+
isAnthropicFastModeFallbackDisabled,
|
|
162
|
+
isClaudeCloakingUserId,
|
|
163
|
+
normalizeAnthropicBaseUrl,
|
|
164
|
+
resolveAnthropicMetadataUserId,
|
|
165
|
+
stripClaudeToolPrefix,
|
|
166
|
+
};
|
|
167
|
+
export { resolvesToOfficialAnthropicEndpoint, supportsAnthropicCompaction, supportsAnthropicCompactionOnClient };
|
|
132
168
|
|
|
133
169
|
export type AnthropicHeaderOptions = {
|
|
134
170
|
apiKey: string;
|
|
@@ -144,15 +180,6 @@ export type AnthropicHeaderOptions = {
|
|
|
144
180
|
allowAnthropicHeaderOverrides?: boolean;
|
|
145
181
|
};
|
|
146
182
|
|
|
147
|
-
export function normalizeAnthropicBaseUrl(baseUrl?: string): string | undefined {
|
|
148
|
-
const trimmed = baseUrl?.trim();
|
|
149
|
-
if (!trimmed) {
|
|
150
|
-
return undefined;
|
|
151
|
-
}
|
|
152
|
-
const withoutTrailingSlashes = trimmed.replace(/\/+$/, "");
|
|
153
|
-
return withoutTrailingSlashes.endsWith("/v1") ? withoutTrailingSlashes.slice(0, -3) : withoutTrailingSlashes;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
183
|
// Build deduplicated beta header string
|
|
157
184
|
export function buildBetaHeader(baseBetas: readonly string[], extraBetas: readonly string[]): string {
|
|
158
185
|
const seen = new Set<string>();
|
|
@@ -416,8 +443,6 @@ type AnthropicOutputConfig = NonNullable<MessageCreateParamsStreaming["output_co
|
|
|
416
443
|
const ANTHROPIC_STOP_SEQUENCES_MAX = 4;
|
|
417
444
|
let warnedStopSequencesTrim = false;
|
|
418
445
|
|
|
419
|
-
const ANTHROPIC_PROVIDER_SESSION_STATE_KEY = "anthropic-messages";
|
|
420
|
-
|
|
421
446
|
/**
|
|
422
447
|
* A mid-conversation `role: "system"` message omp inserts at a fixed slot in
|
|
423
448
|
* the wire history so top-level `tools` and `output_config.effort` can stay
|
|
@@ -504,18 +529,6 @@ function createAnthropicProviderSessionState(): AnthropicProviderSessionState {
|
|
|
504
529
|
return state;
|
|
505
530
|
}
|
|
506
531
|
|
|
507
|
-
/**
|
|
508
|
-
* Key the sticky strict-tools / fast-mode learning per endpoint+model. A
|
|
509
|
-
* grammar-too-large 400 or a fast-mode rejection is specific to the model (its
|
|
510
|
-
* tool grammar / entitlement) and the endpoint (direct Anthropic vs a gateway /
|
|
511
|
-
* Foundry / Bedrock proxy), so it MUST NOT bleed onto unrelated anthropic-messages
|
|
512
|
-
* requests in the same session. NUL separates the two components so neither can
|
|
513
|
-
* forge the boundary.
|
|
514
|
-
*/
|
|
515
|
-
function anthropicProviderSessionStateKey(baseUrl: string, modelId: string): string {
|
|
516
|
-
return `${ANTHROPIC_PROVIDER_SESSION_STATE_KEY}:${baseUrl}\u0000${modelId}`;
|
|
517
|
-
}
|
|
518
|
-
|
|
519
532
|
function getAnthropicProviderSessionState(
|
|
520
533
|
providerSessionState: Map<string, ProviderSessionState> | undefined,
|
|
521
534
|
baseUrl: string,
|
|
@@ -534,41 +547,6 @@ function getAnthropicProviderSessionState(
|
|
|
534
547
|
return created;
|
|
535
548
|
}
|
|
536
549
|
|
|
537
|
-
/**
|
|
538
|
-
* Clears the in-session "server rejected fast mode" sticky flag. Call when the
|
|
539
|
-
* caller is explicitly re-arming `serviceTier: "priority"` (e.g. user toggled
|
|
540
|
-
* `/fast on` after a previous turn auto-disabled it) so the next request
|
|
541
|
-
* actually carries `speed: "fast"` again. No-op when the map or state entry
|
|
542
|
-
* hasn't been materialized yet.
|
|
543
|
-
*/
|
|
544
|
-
export function clearAnthropicFastModeFallback(
|
|
545
|
-
providerSessionState: Map<string, ProviderSessionState> | undefined,
|
|
546
|
-
): void {
|
|
547
|
-
if (!providerSessionState) return;
|
|
548
|
-
// Fast mode is re-armed session-wide (user toggled `/fast on`), so clear the
|
|
549
|
-
// sticky flag on every per-endpoint/model Anthropic entry — plus the legacy
|
|
550
|
-
// unscoped key — rather than a single shared object.
|
|
551
|
-
const prefix = `${ANTHROPIC_PROVIDER_SESSION_STATE_KEY}:`;
|
|
552
|
-
for (const [key, value] of providerSessionState) {
|
|
553
|
-
if (key !== ANTHROPIC_PROVIDER_SESSION_STATE_KEY && !key.startsWith(prefix)) continue;
|
|
554
|
-
(value as AnthropicProviderSessionState).fastModeDisabled = false;
|
|
555
|
-
}
|
|
556
|
-
}
|
|
557
|
-
/**
|
|
558
|
-
* Whether the direct Anthropic model's endpoint-scoped fast-mode fallback is
|
|
559
|
-
* currently active. Reading the map directly is intentional: inspection must
|
|
560
|
-
* not materialize a state entry for a model that has never streamed.
|
|
561
|
-
*/
|
|
562
|
-
export function isAnthropicFastModeFallbackDisabled(
|
|
563
|
-
providerSessionState: Map<string, ProviderSessionState> | undefined,
|
|
564
|
-
model: Model<Api>,
|
|
565
|
-
): boolean {
|
|
566
|
-
if (!providerSessionState || model.provider !== "anthropic" || model.api !== "anthropic-messages") return false;
|
|
567
|
-
const baseUrl = resolveAnthropicBaseUrl(model as Model<"anthropic-messages">) ?? "https://api.anthropic.com";
|
|
568
|
-
const key = anthropicProviderSessionStateKey(baseUrl, model.id);
|
|
569
|
-
return (providerSessionState.get(key) as AnthropicProviderSessionState | undefined)?.fastModeDisabled ?? false;
|
|
570
|
-
}
|
|
571
|
-
|
|
572
550
|
function hasStrictAnthropicTools(params: MessageCreateParamsStreaming): boolean {
|
|
573
551
|
return params.tools?.some(tool => tool.strict === true) ?? false;
|
|
574
552
|
}
|
|
@@ -760,145 +738,8 @@ export function wrapFetchForCch(base: FetchImpl): FetchImpl {
|
|
|
760
738
|
};
|
|
761
739
|
}
|
|
762
740
|
|
|
763
|
-
const CLAUDE_CLOAKING_USER_ID_REGEX =
|
|
764
|
-
/^user_[0-9a-fA-F]{64}_account_[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}_session_[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
765
|
-
|
|
766
|
-
export function isClaudeCloakingUserId(userId: string): boolean {
|
|
767
|
-
return CLAUDE_CLOAKING_USER_ID_REGEX.test(userId);
|
|
768
|
-
}
|
|
769
|
-
|
|
770
|
-
/**
|
|
771
|
-
* Real Claude Code sends `metadata.user_id` as a JSON-stringified object of the
|
|
772
|
-
* shape `{ device_id, account_uuid, session_id, ...extra }` (see
|
|
773
|
-
* services/api/claude.ts → getAPIMetadata). Accept that shape so callers that
|
|
774
|
-
* supply a stable `session_id` aren't silently overwritten with fresh entropy
|
|
775
|
-
* on every request, which would inflate the backend session count.
|
|
776
|
-
*/
|
|
777
|
-
function isClaudeJsonUserId(userId: string): boolean {
|
|
778
|
-
if (userId.length === 0 || userId[0] !== "{") return false;
|
|
779
|
-
let parsed: unknown;
|
|
780
|
-
try {
|
|
781
|
-
parsed = JSON.parse(userId);
|
|
782
|
-
} catch {
|
|
783
|
-
return false;
|
|
784
|
-
}
|
|
785
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return false;
|
|
786
|
-
const obj = parsed as Record<string, unknown>;
|
|
787
|
-
return typeof obj.session_id === "string" && obj.session_id.length > 0;
|
|
788
|
-
}
|
|
789
|
-
|
|
790
|
-
function extractClaudeMetadataSessionId(userId: unknown): string | undefined {
|
|
791
|
-
if (typeof userId !== "string") return undefined;
|
|
792
|
-
if (isClaudeCloakingUserId(userId)) {
|
|
793
|
-
return userId.slice(userId.lastIndexOf("_session_") + "_session_".length);
|
|
794
|
-
}
|
|
795
|
-
if (userId.length === 0 || userId[0] !== "{") return undefined;
|
|
796
|
-
let parsed: unknown;
|
|
797
|
-
try {
|
|
798
|
-
parsed = JSON.parse(userId);
|
|
799
|
-
} catch {
|
|
800
|
-
return undefined;
|
|
801
|
-
}
|
|
802
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return undefined;
|
|
803
|
-
const sessionId = (parsed as Record<string, unknown>).session_id;
|
|
804
|
-
return typeof sessionId === "string" && sessionId.length > 0 ? sessionId : undefined;
|
|
805
|
-
}
|
|
806
|
-
|
|
807
|
-
export function generateClaudeCloakingUserId(): string {
|
|
808
|
-
const userHash = nodeCrypto.randomBytes(32).toString("hex");
|
|
809
|
-
const accountId = nodeCrypto.randomUUID().toLowerCase();
|
|
810
|
-
const sessionId = nodeCrypto.randomUUID().toLowerCase();
|
|
811
|
-
return `user_${userHash}_account_${accountId}_session_${sessionId}`;
|
|
812
|
-
}
|
|
813
|
-
|
|
814
|
-
const CLAUDE_DEVICE_ID_INSTALL_HASH_DOMAIN = "omp-claude-device-id-v1:";
|
|
815
|
-
const CLAUDE_DEVICE_ID_ACCOUNT_HASH_DOMAIN = "omp-claude-device-id-v2";
|
|
816
|
-
|
|
817
|
-
export function deriveClaudeDeviceId(installId: string, accountId?: string): string {
|
|
818
|
-
const hash = nodeCrypto.createHash("sha256");
|
|
819
|
-
if (accountId && accountId.length > 0) {
|
|
820
|
-
return hash
|
|
821
|
-
.update(CLAUDE_DEVICE_ID_ACCOUNT_HASH_DOMAIN)
|
|
822
|
-
.update("\0")
|
|
823
|
-
.update(installId)
|
|
824
|
-
.update("\0")
|
|
825
|
-
.update(accountId)
|
|
826
|
-
.digest("hex");
|
|
827
|
-
}
|
|
828
|
-
return hash.update(CLAUDE_DEVICE_ID_INSTALL_HASH_DOMAIN).update(installId).digest("hex");
|
|
829
|
-
}
|
|
830
|
-
|
|
831
|
-
function readMetadataString(metadata: Record<string, unknown> | undefined, key: string): string | undefined {
|
|
832
|
-
const value = metadata?.[key];
|
|
833
|
-
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
834
|
-
}
|
|
835
|
-
|
|
836
|
-
function readAnthropicMetadataAccountId(metadata: Record<string, unknown> | undefined): string | undefined {
|
|
837
|
-
return (
|
|
838
|
-
readMetadataString(metadata, "account_uuid") ??
|
|
839
|
-
readMetadataString(metadata, "accountId") ??
|
|
840
|
-
readMetadataString(metadata, "account_id")
|
|
841
|
-
);
|
|
842
|
-
}
|
|
843
|
-
|
|
844
|
-
function deriveClaudeDeviceIdFromInstallId(accountId?: string): string {
|
|
845
|
-
return deriveClaudeDeviceId(getInstallId(), accountId);
|
|
846
|
-
}
|
|
847
|
-
|
|
848
|
-
function generateClaudeJsonUserId(sessionId?: string, accountId?: string): string {
|
|
849
|
-
const userId: Record<string, string> = {
|
|
850
|
-
device_id: deriveClaudeDeviceIdFromInstallId(accountId),
|
|
851
|
-
session_id: sessionId ?? nodeCrypto.randomUUID().toLowerCase(),
|
|
852
|
-
};
|
|
853
|
-
if (accountId && accountId.length > 0) userId.account_uuid = accountId;
|
|
854
|
-
return JSON.stringify(userId);
|
|
855
|
-
}
|
|
856
|
-
|
|
857
|
-
/**
|
|
858
|
-
* Resolve the `metadata.user_id` field for an Anthropic Messages request.
|
|
859
|
-
*
|
|
860
|
-
* For API-key tokens, an explicit caller-supplied `userId` is forwarded
|
|
861
|
-
* verbatim and `undefined` yields no metadata. For OAuth tokens the value
|
|
862
|
-
* must match the Claude Code attribution shape (`isClaudeCloakingUserId` or
|
|
863
|
-
* the `{session_id, account_uuid?, device_id?}` JSON envelope) — anything
|
|
864
|
-
* else is dropped and a fresh Claude-Code-style JSON id is generated from
|
|
865
|
-
* `sessionId`/`accountId` so attribution stays consistent across the main
|
|
866
|
-
* streaming path and provider-specific request builders (e.g. web search).
|
|
867
|
-
*/
|
|
868
|
-
export function resolveAnthropicMetadataUserId(
|
|
869
|
-
userId: unknown,
|
|
870
|
-
isOAuthToken: boolean,
|
|
871
|
-
sessionId?: string,
|
|
872
|
-
accountId?: string,
|
|
873
|
-
): string | undefined {
|
|
874
|
-
if (typeof userId === "string") {
|
|
875
|
-
if (!isOAuthToken || isClaudeCloakingUserId(userId) || isClaudeJsonUserId(userId)) {
|
|
876
|
-
return userId;
|
|
877
|
-
}
|
|
878
|
-
}
|
|
879
|
-
|
|
880
|
-
if (!isOAuthToken) return undefined;
|
|
881
|
-
return generateClaudeJsonUserId(sessionId, accountId);
|
|
882
|
-
}
|
|
883
|
-
const ANTHROPIC_BUILTIN_TOOL_NAMES = new Set(["web_search", "code_execution", "text_editor", "computer"]);
|
|
884
741
|
const UMANS_WEBSEARCH_PROVIDER_HEADER = "X-Umans-Websearch-Provider";
|
|
885
742
|
const UMANS_WEBSEARCH_TOOL_NAME = "web_search";
|
|
886
|
-
export const applyClaudeToolPrefix = (name: string): string => {
|
|
887
|
-
if (!claudeToolPrefix) return name;
|
|
888
|
-
if (ANTHROPIC_BUILTIN_TOOL_NAMES.has(name.toLowerCase())) return name;
|
|
889
|
-
// Always prepend (no "already prefixed" short-circuit): the prefix is a wire
|
|
890
|
-
// transport detail applied once to internal tool names, and `stripClaudeToolPrefix`
|
|
891
|
-
// removes exactly one prefix on receive. Skipping names that already start with the
|
|
892
|
-
// prefix would make a tool literally named `_foo` lose its leading underscore on the
|
|
893
|
-
// return trip (`_foo` → wire `_foo` → strip → `foo`), so the agent loop can't find it.
|
|
894
|
-
return `${claudeToolPrefix}${name}`;
|
|
895
|
-
};
|
|
896
|
-
|
|
897
|
-
export const stripClaudeToolPrefix = (name: string): string => {
|
|
898
|
-
if (!claudeToolPrefix) return name;
|
|
899
|
-
if (!name.toLowerCase().startsWith(claudeToolPrefix.toLowerCase())) return name;
|
|
900
|
-
return name.slice(claudeToolPrefix.length);
|
|
901
|
-
};
|
|
902
743
|
|
|
903
744
|
function normalizeUmansWebSearchProvider(value: string | undefined): "native" | "exa" | undefined {
|
|
904
745
|
const normalized = value?.trim().toLowerCase();
|
|
@@ -1327,22 +1168,7 @@ function resolveAnthropicBaseUrl(model: Model<"anthropic-messages">, apiKey?: st
|
|
|
1327
1168
|
if (model.provider === "github-copilot") {
|
|
1328
1169
|
return normalizeAnthropicBaseUrl(resolveGitHubCopilotBaseUrl(model.baseUrl, apiKey) ?? model.baseUrl);
|
|
1329
1170
|
}
|
|
1330
|
-
if (model.provider === "anthropic"
|
|
1331
|
-
const foundryBaseUrl = normalizeAnthropicBaseUrl($env.FOUNDRY_BASE_URL);
|
|
1332
|
-
if (foundryBaseUrl) {
|
|
1333
|
-
return foundryBaseUrl;
|
|
1334
|
-
}
|
|
1335
|
-
}
|
|
1336
|
-
if (model.provider === "anthropic") {
|
|
1337
|
-
const configured = normalizeAnthropicBaseUrl(model.baseUrl);
|
|
1338
|
-
// An explicitly configured non-official baseUrl (e.g. a models.yml provider
|
|
1339
|
-
// override) is more specific than the generic env fallback and wins.
|
|
1340
|
-
if (configured && !isOfficialAnthropicApiUrl(configured)) return configured;
|
|
1341
|
-
// Otherwise ANTHROPIC_BASE_URL routes chat through an enterprise gateway
|
|
1342
|
-
// (docs/environment-variables.md), ahead of the official default. The
|
|
1343
|
-
// Foundry redirect is already handled above.
|
|
1344
|
-
return normalizeAnthropicBaseUrl($env.ANTHROPIC_BASE_URL) ?? configured ?? "https://api.anthropic.com";
|
|
1345
|
-
}
|
|
1171
|
+
if (model.provider === "anthropic") return resolveDirectAnthropicBaseUrl(model);
|
|
1346
1172
|
return normalizeAnthropicBaseUrl(model.baseUrl);
|
|
1347
1173
|
}
|
|
1348
1174
|
|
|
@@ -1791,89 +1617,6 @@ function parseAnthropicFallbackWireBlock(value: unknown): AnthropicFallbackConte
|
|
|
1791
1617
|
|
|
1792
1618
|
const ANTHROPIC_COMPACTION_MIN_TRIGGER_TOKENS = 50_000;
|
|
1793
1619
|
|
|
1794
|
-
/**
|
|
1795
|
-
* Whether this model's requests reach the official Anthropic API, resolved the
|
|
1796
|
-
* way the transport resolves it — including the Foundry and
|
|
1797
|
-
* `ANTHROPIC_BASE_URL` reroutes that leave `compat.officialEndpoint` stale.
|
|
1798
|
-
*/
|
|
1799
|
-
export function resolvesToOfficialAnthropicEndpoint(model: Model<"anthropic-messages">): boolean {
|
|
1800
|
-
return isOfficialAnthropicApiUrl(resolveAnthropicBaseUrl(model));
|
|
1801
|
-
}
|
|
1802
|
-
|
|
1803
|
-
/**
|
|
1804
|
-
* Whether server-side compaction (`compact-2026-01-12`) may be spoken for
|
|
1805
|
-
* this model to the endpoint a request actually reaches: a model line the
|
|
1806
|
-
* beta supports (`compat.supportsServerCompaction`, rule-owned in the
|
|
1807
|
-
* catalog), on the official API for the first-party provider or on any
|
|
1808
|
-
* endpoint that opted in through `remoteCompaction.enabled`, and never on one
|
|
1809
|
-
* whose deployment contract excludes context management. The same predicate
|
|
1810
|
-
* gates emitting the edit, attaching the beta, and replaying a persisted
|
|
1811
|
-
* block, so a route or model change can never leave a session sending a block
|
|
1812
|
-
* its endpoint rejects.
|
|
1813
|
-
*/
|
|
1814
|
-
export function supportsAnthropicCompaction(model: Model<"anthropic-messages">, effectiveBaseUrl?: string): boolean {
|
|
1815
|
-
if (!isCompactionCapableModel(model)) return false;
|
|
1816
|
-
if (model.remoteCompaction?.enabled === true) return true;
|
|
1817
|
-
// First-party provider is catalog policy (`first-party-provider` on the
|
|
1818
|
-
// provider rules), never a provider-id literal. It reads its own axis
|
|
1819
|
-
// rather than `officialEndpoint`, which stays URL-derived.
|
|
1820
|
-
// A `transport: "pi-native"` baseUrl names the auth gateway, not the
|
|
1821
|
-
// upstream model server: the gateway resolves the model's own provider
|
|
1822
|
-
// server-side, so the upstream URL check cannot apply to the model's own
|
|
1823
|
-
// transport URL — but only for the KDL-owned first-party deployment. A
|
|
1824
|
-
// custom provider travels the same gateway to its own upstream, whose
|
|
1825
|
-
// server-side gate stays off (unless `remoteCompaction.enabled` opts the
|
|
1826
|
-
// route in above). An explicitly supplied foreign endpoint (e.g. a
|
|
1827
|
-
// caller-owned client's URL) is still judged on its own merits below.
|
|
1828
|
-
if (
|
|
1829
|
-
model.transport === "pi-native" &&
|
|
1830
|
-
model.compat.firstPartyProvider === true &&
|
|
1831
|
-
(effectiveBaseUrl === undefined || effectiveBaseUrl === normalizeAnthropicBaseUrl(model.baseUrl))
|
|
1832
|
-
) {
|
|
1833
|
-
return true;
|
|
1834
|
-
}
|
|
1835
|
-
return (
|
|
1836
|
-
model.compat.firstPartyProvider === true &&
|
|
1837
|
-
(effectiveBaseUrl === undefined
|
|
1838
|
-
? resolvesToOfficialAnthropicEndpoint(model)
|
|
1839
|
-
: isOfficialAnthropicApiUrl(effectiveBaseUrl))
|
|
1840
|
-
);
|
|
1841
|
-
}
|
|
1842
|
-
|
|
1843
|
-
/**
|
|
1844
|
-
* {@link supportsAnthropicCompaction} for a request on a caller-owned client:
|
|
1845
|
-
* the endpoint is whatever the client targets (an `AnthropicVertex` client
|
|
1846
|
-
* carries an Anthropic model to Vertex), never the model's own routing. SDK
|
|
1847
|
-
* clients expose it as `baseURL`; a client that exposes no endpoint only
|
|
1848
|
-
* compacts through an explicit `remoteCompaction.enabled` opt-in.
|
|
1849
|
-
*/
|
|
1850
|
-
export function supportsAnthropicCompactionOnClient(
|
|
1851
|
-
model: Model<"anthropic-messages">,
|
|
1852
|
-
client: AnthropicMessagesClientLike,
|
|
1853
|
-
): boolean {
|
|
1854
|
-
const baseURL = injectedClientBaseUrl(client);
|
|
1855
|
-
if (baseURL !== undefined) return supportsAnthropicCompaction(model, baseURL);
|
|
1856
|
-
return isCompactionCapableModel(model) && model.remoteCompaction?.enabled === true;
|
|
1857
|
-
}
|
|
1858
|
-
|
|
1859
|
-
/**
|
|
1860
|
-
* Effective endpoint of a caller-owned client. SDK clients expose it as
|
|
1861
|
-
* `baseURL`; a client that exposes none leaves routing to the model.
|
|
1862
|
-
*/
|
|
1863
|
-
function injectedClientBaseUrl(client: AnthropicMessagesClientLike): string | undefined {
|
|
1864
|
-
const baseURL = (client as { baseURL?: unknown }).baseURL;
|
|
1865
|
-
return typeof baseURL === "string" && baseURL.length > 0 ? baseURL : undefined;
|
|
1866
|
-
}
|
|
1867
|
-
|
|
1868
|
-
/** The model-side half of the gate: lineage support and a deployment contract that allows it. */
|
|
1869
|
-
function isCompactionCapableModel(model: Model<"anthropic-messages">): boolean {
|
|
1870
|
-
return (
|
|
1871
|
-
model.compat.supportsServerCompaction === true &&
|
|
1872
|
-
model.compat.supportsContextManagement !== false &&
|
|
1873
|
-
model.remoteCompaction?.enabled !== false
|
|
1874
|
-
);
|
|
1875
|
-
}
|
|
1876
|
-
|
|
1877
1620
|
/**
|
|
1878
1621
|
* Whether a persisted compaction summary replays as a native `compaction`
|
|
1879
1622
|
* block: only the provider that produced it may replay it, and only on a
|
|
@@ -3954,8 +3697,17 @@ function applyPromptCaching(params: MessageCreateParamsStreaming, cacheControl?:
|
|
|
3954
3697
|
const messageEnd = hasTrailingAssistantPad ? trailingIndex - 1 : trailingIndex;
|
|
3955
3698
|
|
|
3956
3699
|
// A breakpoint caches every preceding byte, not only the decorated message.
|
|
3957
|
-
//
|
|
3958
|
-
//
|
|
3700
|
+
// A per-call or turn-scoped message is rebuilt next request, so a prefix
|
|
3701
|
+
// spanning it cannot match — but only at its own position. Messages after
|
|
3702
|
+
// the mark are ordinary persisted history with stable bytes, so a later
|
|
3703
|
+
// breakpoint still matches everything after the mark. The cost is bounded
|
|
3704
|
+
// to re-billing the marked bytes themselves, not the growing tail.
|
|
3705
|
+
// Hence two anchors, not a truncation: the newest candidate at or before
|
|
3706
|
+
// the first per-call/turn-scoped message (when one exists) pins the
|
|
3707
|
+
// reusable prefix behind the mark, and the rolling tail candidates pin
|
|
3708
|
+
// the suffix after it. Turn-scoped `clear_at` messages are absent next
|
|
3709
|
+
// request, so they still truncate the decimation range (ordinals would
|
|
3710
|
+
// shift), but per-call marks no longer freeze the tail.
|
|
3959
3711
|
let stableMessageEnd = messageEnd;
|
|
3960
3712
|
for (let index = 0; index <= messageEnd; index++) {
|
|
3961
3713
|
const message = params.messages[index];
|
|
@@ -3980,17 +3732,21 @@ function applyPromptCaching(params: MessageCreateParamsStreaming, cacheControl?:
|
|
|
3980
3732
|
// Stable historical decimation checkpoint every 15 user turns (15th, 30th, 45th...)
|
|
3981
3733
|
const decimationIndices = userIndices.filter((_, ordinal) => (ordinal + 1) % ANTHROPIC_DECIMATION_INTERVAL === 0);
|
|
3982
3734
|
|
|
3983
|
-
// Collect up to 2 trailing candidates from the
|
|
3984
|
-
//
|
|
3985
|
-
//
|
|
3986
|
-
//
|
|
3987
|
-
//
|
|
3988
|
-
//
|
|
3989
|
-
//
|
|
3735
|
+
// Collect up to 2 trailing candidates from the message tail, skipping
|
|
3736
|
+
// per-call messages, turn-scoped messages, and mid-conversation
|
|
3737
|
+
// tool-control messages. A per-call tail candidate is rebuilt next request
|
|
3738
|
+
// (fresh timestamps on appended probes, fresh redaction bytes), so a
|
|
3739
|
+
// breakpoint on it cannot match — it would spend the tail anchor on bytes
|
|
3740
|
+
// that never repeat while the persisted history behind it goes uncached.
|
|
3741
|
+
// Turn-scoped messages are absent next request for the same reason, and
|
|
3742
|
+
// tool controls reject cache_control outright. The walk starts at the
|
|
3743
|
+
// message tail (not the truncated prefix end) so the anchor advances every
|
|
3744
|
+
// turn; the sub-prefix candidate below covers the reusable region behind
|
|
3745
|
+
// a mark.
|
|
3990
3746
|
const trailingCandidates: number[] = [];
|
|
3991
|
-
for (let index =
|
|
3747
|
+
for (let index = messageEnd; index >= 0 && trailingCandidates.length < 2; index--) {
|
|
3992
3748
|
const message = params.messages[index];
|
|
3993
|
-
if (!message) continue;
|
|
3749
|
+
if (!message || message.clear_at === "next_user_message" || isPerCallContextMessage(message)) continue;
|
|
3994
3750
|
if (
|
|
3995
3751
|
message.role === "system" &&
|
|
3996
3752
|
typeof message.content !== "string" &&
|
|
@@ -4002,11 +3758,13 @@ function applyPromptCaching(params: MessageCreateParamsStreaming, cacheControl?:
|
|
|
4002
3758
|
}
|
|
4003
3759
|
trailingCandidates.push(index);
|
|
4004
3760
|
}
|
|
4005
|
-
|
|
4006
3761
|
// Prioritize:
|
|
4007
3762
|
// 1. Most recent trailing message
|
|
4008
3763
|
// 2. Latest decimation checkpoints (newest first) to maintain stable long-context anchors
|
|
4009
|
-
// 3.
|
|
3764
|
+
// 3. Newest message at or before the first per-call/turn-scoped mark, so a
|
|
3765
|
+
// volatile interior message costs only its own re-billed bytes instead
|
|
3766
|
+
// of invalidating the whole reusable prefix behind it
|
|
3767
|
+
// 4. Second trailing message
|
|
4010
3768
|
const candidateIndices: number[] = [];
|
|
4011
3769
|
if (trailingCandidates.length > 0) {
|
|
4012
3770
|
candidateIndices.push(trailingCandidates[0]);
|
|
@@ -4016,6 +3774,9 @@ function applyPromptCaching(params: MessageCreateParamsStreaming, cacheControl?:
|
|
|
4016
3774
|
candidateIndices.push(decimationIndices[i]);
|
|
4017
3775
|
}
|
|
4018
3776
|
}
|
|
3777
|
+
if (stableMessageEnd < messageEnd && stableMessageEnd >= 0 && !candidateIndices.includes(stableMessageEnd)) {
|
|
3778
|
+
candidateIndices.push(stableMessageEnd);
|
|
3779
|
+
}
|
|
4019
3780
|
for (const index of trailingCandidates) {
|
|
4020
3781
|
if (!candidateIndices.includes(index)) {
|
|
4021
3782
|
candidateIndices.push(index);
|
|
@@ -4034,17 +3795,57 @@ function applyPromptCaching(params: MessageCreateParamsStreaming, cacheControl?:
|
|
|
4034
3795
|
}
|
|
4035
3796
|
}
|
|
4036
3797
|
|
|
3798
|
+
/**
|
|
3799
|
+
* Trailing system-prompt segments carrying per-turn volatile content (memory
|
|
3800
|
+
* recall blocks). They are rendered by the coding agent as their own
|
|
3801
|
+
* `systemPrompt` array elements and appended last, so on the wire they
|
|
3802
|
+
* normally form a volatile suffix after the stable prefix. The system cache
|
|
3803
|
+
* breakpoint anchors on the last stable segment instead of the array tail, so
|
|
3804
|
+
* a recall refresh re-bills only the suffix and the message tail for one turn
|
|
3805
|
+
* while the tools+stable-system prefix stays a cache hit. The fingerprint in
|
|
3806
|
+
* `planStableAnthropicSystem` is scoped the same way, so a recall-only change
|
|
3807
|
+
* no longer resets the tool/control baselines either.
|
|
3808
|
+
*
|
|
3809
|
+
* Only a genuinely trailing volatile run counts: a `before_agent_start`
|
|
3810
|
+
* extension override may append a stable policy block after the staged recall
|
|
3811
|
+
* block, and that block must stay fingerprinted stable (a change to it has to
|
|
3812
|
+
* re-baseline). A volatile block stranded mid-array still poisons the prefix
|
|
3813
|
+
* at its position — prefix caching is positional, so no classification can
|
|
3814
|
+
* save the bytes after it — but the stable tail is at least fingerprinted
|
|
3815
|
+
* instead of silently excluded.
|
|
3816
|
+
*
|
|
3817
|
+
* Detection is by our own markup, not model identity: recall blocks always
|
|
3818
|
+
* open with `<memories>`. Stable segments containing recalled text elsewhere
|
|
3819
|
+
* (e.g. quoted in conversation) are unaffected — only a leading tag counts.
|
|
3820
|
+
*/
|
|
3821
|
+
const VOLATILE_SYSTEM_SEGMENT_MARKERS = ["<memories>"];
|
|
3822
|
+
|
|
3823
|
+
function stableSystemSuffixStart(systemBlocks: readonly AnthropicSystemBlock[]): number {
|
|
3824
|
+
let start = systemBlocks.length;
|
|
3825
|
+
while (start > 0) {
|
|
3826
|
+
const text = systemBlocks[start - 1]?.text ?? "";
|
|
3827
|
+
if (!VOLATILE_SYSTEM_SEGMENT_MARKERS.some(marker => text.startsWith(marker))) break;
|
|
3828
|
+
start--;
|
|
3829
|
+
}
|
|
3830
|
+
return start;
|
|
3831
|
+
}
|
|
3832
|
+
|
|
4037
3833
|
/**
|
|
4038
3834
|
* Anchor cache_control on the stable request head — the last (non-deferred)
|
|
4039
|
-
* tool definition and the last system block. The canonical cache order is
|
|
4040
|
-
* tools → system → messages, so a breakpoint on the final system block caches
|
|
4041
|
-
* the entire tools+system prefix, and the extra tool breakpoint keeps the tool
|
|
3835
|
+
* tool definition and the last stable system block. The canonical cache order is
|
|
3836
|
+
* tools → system → messages, so a breakpoint on the final stable system block caches
|
|
3837
|
+
* the entire tools+stable-system prefix, and the extra tool breakpoint keeps the tool
|
|
4042
3838
|
* definitions cached even when the system text changes. This guarantees the
|
|
4043
3839
|
* large, unchanging head is a cache hit on every turn regardless of how the
|
|
4044
3840
|
* message tail churns — the breakpoint placement first-party Anthropic clients
|
|
4045
3841
|
* (Claude Code, Pi) use. Without it, the general API-key path anchors only the
|
|
4046
3842
|
* moving message tail, so tail churn re-writes the whole head uncached.
|
|
4047
3843
|
*
|
|
3844
|
+
* Volatile trailing segments (memory recall) sit after the breakpoint, so a
|
|
3845
|
+
* recall refresh re-bills only the suffix and the tail for one turn instead of
|
|
3846
|
+
* the whole head. When every system block is volatile there is no stable
|
|
3847
|
+
* boundary and the breakpoint stays on the array tail (previous behavior).
|
|
3848
|
+
*
|
|
4048
3849
|
* Anthropic allows at most 4 cache breakpoints per request. At most one is
|
|
4049
3850
|
* spent on tools and one on system here, leaving the remaining budget for
|
|
4050
3851
|
* the message tail and historical decimation checkpoints in `applyPromptCaching`.
|
|
@@ -4080,9 +3881,28 @@ function applyHeadCaching(
|
|
|
4080
3881
|
}
|
|
4081
3882
|
}
|
|
4082
3883
|
|
|
4083
|
-
if (systemBlocks && systemBlocks.length > 0
|
|
4084
|
-
|
|
4085
|
-
|
|
3884
|
+
if (systemBlocks && systemBlocks.length > 0) {
|
|
3885
|
+
// Anchor on the last stable block so a volatile recall suffix refresh
|
|
3886
|
+
// re-bills only the suffix, not the whole head. The skip-if-decorated
|
|
3887
|
+
// check applies only when there is no volatile suffix (previous
|
|
3888
|
+
// behavior): with a suffix present the boundary anchor is added
|
|
3889
|
+
// whenever the anchor block itself lacks a breakpoint, even if the
|
|
3890
|
+
// OAuth path pre-decorated its identity block — otherwise the only
|
|
3891
|
+
// system breakpoint sits before the stable prompt and a recall
|
|
3892
|
+
// refresh re-bills it. The message budget in `applyPromptCaching`
|
|
3893
|
+
// shrinks accordingly (4 minus head breakpoints). All-volatile falls
|
|
3894
|
+
// back to tail anchoring (previous behavior).
|
|
3895
|
+
const suffixStart = stableSystemSuffixStart(systemBlocks);
|
|
3896
|
+
if (suffixStart === systemBlocks.length) {
|
|
3897
|
+
if (!systemBlocks.some(block => block.cache_control != null)) {
|
|
3898
|
+
const lastBlock = systemBlocks[systemBlocks.length - 1];
|
|
3899
|
+
if (lastBlock) lastBlock.cache_control = cloneAnthropicCacheControl(cacheControl);
|
|
3900
|
+
}
|
|
3901
|
+
} else {
|
|
3902
|
+
const anchorIndex = suffixStart === 0 ? systemBlocks.length - 1 : suffixStart - 1;
|
|
3903
|
+
const anchor = systemBlocks[anchorIndex];
|
|
3904
|
+
if (anchor && anchor.cache_control == null) anchor.cache_control = cloneAnthropicCacheControl(cacheControl);
|
|
3905
|
+
}
|
|
4086
3906
|
}
|
|
4087
3907
|
}
|
|
4088
3908
|
|
|
@@ -4166,11 +3986,15 @@ function getAnthropicControlState(
|
|
|
4166
3986
|
): AnthropicControlState | undefined {
|
|
4167
3987
|
if (!state) return undefined;
|
|
4168
3988
|
const root = messages[0];
|
|
3989
|
+
// Key on the stable system prefix, not the full array: a volatile recall
|
|
3990
|
+
// suffix refresh must resolve the same baseline or the declared-tool,
|
|
3991
|
+
// effort, and control-transition state it preserves is lost with it.
|
|
3992
|
+
const stablePrefix = system?.slice(0, stableSystemSuffixStart(system)) ?? null;
|
|
4169
3993
|
const fingerprint = String(
|
|
4170
3994
|
Bun.hash(
|
|
4171
3995
|
JSON.stringify([
|
|
4172
3996
|
sessionId ?? "",
|
|
4173
|
-
|
|
3997
|
+
stablePrefix?.map(block => block.text) ?? null,
|
|
4174
3998
|
root ? anthropicControlMessageProjection(root) : null,
|
|
4175
3999
|
]),
|
|
4176
4000
|
),
|
|
@@ -4216,14 +4040,16 @@ function syncAnthropicControlState(state: AnthropicControlState, messages: reado
|
|
|
4216
4040
|
}
|
|
4217
4041
|
|
|
4218
4042
|
/**
|
|
4219
|
-
* Keep the top-level `system` array byte-stable across a session. The
|
|
4220
|
-
* captured on the first request
|
|
4221
|
-
* request's cache breakpoints) while
|
|
4043
|
+
* Keep the top-level `system` array byte-stable across a session. The stable
|
|
4044
|
+
* prefix captured on the first request is replayed verbatim (with the current
|
|
4045
|
+
* request's cache breakpoints) while its text is unchanged; the volatile
|
|
4046
|
+
* recall suffix always passes through current-turn. A stable-prefix change
|
|
4222
4047
|
* re-baselines instead of duplicating the prompt as a mid-conversation
|
|
4223
4048
|
* system message: omp's system prompt is one rendered segment that embeds
|
|
4224
4049
|
* the tool roster, so replaying a second copy on every later request would
|
|
4225
4050
|
* cost the full prompt again per change. The prefix rewrite is absorbed by
|
|
4226
|
-
* `prefix_mismatch_behavior: "drop_block"` and one cache miss
|
|
4051
|
+
* `prefix_mismatch_behavior: "drop_block"` and one cache miss, while a
|
|
4052
|
+
* recall-only change keeps the tool/control baselines intact.
|
|
4227
4053
|
*/
|
|
4228
4054
|
function planStableAnthropicSystem(
|
|
4229
4055
|
current: AnthropicSystemBlock[] | undefined,
|
|
@@ -4231,16 +4057,21 @@ function planStableAnthropicSystem(
|
|
|
4231
4057
|
enabled: boolean,
|
|
4232
4058
|
): AnthropicSystemBlock[] | undefined {
|
|
4233
4059
|
if (!state || !enabled) return current;
|
|
4234
|
-
const
|
|
4060
|
+
const suffixStart = stableSystemSuffixStart(current ?? []);
|
|
4061
|
+
const fingerprint = JSON.stringify(current?.slice(0, suffixStart).map(block => block.text) ?? null);
|
|
4235
4062
|
if (state.systemFingerprint !== fingerprint) {
|
|
4236
4063
|
resetAnthropicControlState(state);
|
|
4237
4064
|
state.systemFingerprint = fingerprint;
|
|
4238
|
-
state.stableSystemBlocks = current?.map(block => ({ type: block.type, text: block.text }));
|
|
4065
|
+
state.stableSystemBlocks = current?.slice(0, suffixStart).map(block => ({ type: block.type, text: block.text }));
|
|
4239
4066
|
}
|
|
4240
|
-
|
|
4241
|
-
|
|
4242
|
-
|
|
4243
|
-
|
|
4067
|
+
const stableReplay =
|
|
4068
|
+
state.stableSystemBlocks?.map((block, index) => {
|
|
4069
|
+
const cacheControl = current?.[index]?.cache_control;
|
|
4070
|
+
return cacheControl ? { ...block, cache_control: cloneAnthropicCacheControl(cacheControl) } : { ...block };
|
|
4071
|
+
}) ?? [];
|
|
4072
|
+
const suffix = current?.slice(suffixStart).map(block => ({ ...block })) ?? [];
|
|
4073
|
+
const replayed = [...stableReplay, ...suffix];
|
|
4074
|
+
return replayed.length > 0 ? replayed : undefined;
|
|
4244
4075
|
}
|
|
4245
4076
|
|
|
4246
4077
|
function anthropicToolDefinitionKey(tool: AnthropicWireTool): string {
|
|
@@ -4479,7 +4310,7 @@ function buildParams(
|
|
|
4479
4310
|
// Pre-compute metadata.
|
|
4480
4311
|
const metadataAccountId = readAnthropicMetadataAccountId(options?.metadata);
|
|
4481
4312
|
const metadataUserId = resolveAnthropicMetadataUserId(
|
|
4482
|
-
|
|
4313
|
+
readAnthropicMetadataString(options?.metadata, "user_id") ??
|
|
4483
4314
|
// Deliberately share the normalized affinity identity across Kimi's two transports.
|
|
4484
4315
|
(model.provider === "kimi-code" ? getOpenAIPromptCacheKey(options) : undefined),
|
|
4485
4316
|
isOAuthToken,
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
+
import { NO_AUTH_SENTINEL } from "../auth-retry";
|
|
1
2
|
import { type AwsBedrockProviderOptions, resolveAwsBearerToken } from "../registry/aws";
|
|
2
3
|
import type { FetchImpl, Model } from "../types";
|
|
3
4
|
import { resolveAwsRegion } from "../utils/aws-profile";
|
|
4
5
|
import { invalidateAwsCredentialCache, resolveAwsCredentials } from "./aws-credentials";
|
|
5
6
|
import { signRequest } from "./aws-sigv4";
|
|
6
7
|
import type { OpenAIResponsesOptions } from "./openai-responses";
|
|
7
|
-
import { NO_AUTH_SENTINEL } from "./openai-shared";
|
|
8
8
|
|
|
9
9
|
export type BedrockMantleProviderOptions = AwsBedrockProviderOptions;
|
|
10
10
|
|
|
@@ -87,10 +87,6 @@ export function clearGitLabDuoDirectAccessCache(): void {
|
|
|
87
87
|
directAccessCache.clear();
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
-
export function isGitLabDuoModel(model: Model<Api>): boolean {
|
|
91
|
-
return model.provider === "gitlab-duo";
|
|
92
|
-
}
|
|
93
|
-
|
|
94
90
|
export function streamGitLabDuo(
|
|
95
91
|
model: Model<Api>,
|
|
96
92
|
context: Context,
|