@jacobbd/relay-ai 0.12.3 → 0.12.5
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/README.md +14 -1
- package/dist/{chunk-C7TTIGMR.js → chunk-2LUDFIZX.js} +484 -12
- package/dist/chunk-2LUDFIZX.js.map +1 -0
- package/dist/{chunk-R427D6VA.js → chunk-S7HYE2FI.js} +155 -16
- package/dist/chunk-S7HYE2FI.js.map +1 -0
- package/dist/{chunk-3K2DFBNP.js → chunk-SJEJAHKL.js} +2 -2
- package/dist/cli.js +24 -15
- package/dist/cli.js.map +1 -1
- package/dist/core/index.js +655 -35
- package/dist/core/index.js.map +1 -1
- package/dist/{manual-models-MN2GWS4I.js → manual-models-WCK2QGLV.js} +3 -3
- package/dist/{ui-command-Q27TKD53.js → ui-command-7AZ7SLTB.js} +4 -4
- package/package.json +1 -1
- package/dist/chunk-C7TTIGMR.js.map +0 -1
- package/dist/chunk-R427D6VA.js.map +0 -1
- /package/dist/{chunk-3K2DFBNP.js.map → chunk-SJEJAHKL.js.map} +0 -0
- /package/dist/{manual-models-MN2GWS4I.js.map → manual-models-WCK2QGLV.js.map} +0 -0
- /package/dist/{ui-command-Q27TKD53.js.map → ui-command-7AZ7SLTB.js.map} +0 -0
package/README.md
CHANGED
|
@@ -564,6 +564,19 @@ For agent / alef-agent integration (boot flags, NDJSON): **[docs/AI-AGENTS.md](d
|
|
|
564
564
|
|
|
565
565
|
When OpenCode Zen is in your registry, `subscriptionFilter` controls which Zen models appear (`free` = free tier only; default = all Zen models). Add or change Zen via `relay-ai providers`.
|
|
566
566
|
|
|
567
|
+
Zen, Go, OpenRouter, and Command Code gateways can expose both Chat
|
|
568
|
+
Completions and Anthropic Messages. Relay uses current model metadata first, so
|
|
569
|
+
newly named models are not forced into a provider-wide format. For requests
|
|
570
|
+
Relay translates (OpenAI-format models, and any model used from Codex, Gemini,
|
|
571
|
+
or Antigravity), an early protocol or gateway error makes Relay retry the same
|
|
572
|
+
provider/account/model once on the sibling endpoint before streaming any
|
|
573
|
+
output. Successful choices are remembered for 24 hours; two failed attempts
|
|
574
|
+
pause protocol retries for five minutes. Authentication, quota, context,
|
|
575
|
+
moderation, and cancellation errors do not trigger a retry. Anthropic-format
|
|
576
|
+
models in Claude Code, and the server's direct routes, are forwarded untouched
|
|
577
|
+
so prompt caching, beta headers, and thinking keep working; they do not retry
|
|
578
|
+
on the sibling endpoint.
|
|
579
|
+
|
|
567
580
|
### Environment isolation
|
|
568
581
|
|
|
569
582
|
When you launch, relay-ai builds a clean child environment:
|
|
@@ -584,7 +597,7 @@ OpenCode exposes models through different API formats. relay-ai handles them whe
|
|
|
584
597
|
|
|
585
598
|
| Model format | Examples | How it works | Label |
|
|
586
599
|
|---|---|---|---|
|
|
587
|
-
| Anthropic native | Claude, Qwen, MiniMax (Go) | Direct connection | *(none)* |
|
|
600
|
+
| Anthropic native | Claude, Union Alpha, Qwen, MiniMax (Go) | Direct connection | *(none)* |
|
|
588
601
|
| OpenAI chat completions | DeepSeek, Kimi, MiMo, GLM, Grok, GPT-4o (OpenCode OpenAI provider) | SDK adapter proxy (Vercel AI SDK) | `via proxy` |
|
|
589
602
|
| OpenAI Responses API | GPT-5.4+, GPT-5.5, Codex, o-series (OpenCode OpenAI provider only) | Same proxy; SDK picks Responses API | `via proxy` |
|
|
590
603
|
| Gemini native | Gemini (OpenCode Google provider) | SDK adapter, Gemini native API | `via proxy` |
|
|
@@ -7,7 +7,7 @@ import { join } from "path";
|
|
|
7
7
|
// package.json
|
|
8
8
|
var package_default = {
|
|
9
9
|
name: "@jacobbd/relay-ai",
|
|
10
|
-
version: "0.12.
|
|
10
|
+
version: "0.12.5",
|
|
11
11
|
publishConfig: {
|
|
12
12
|
access: "public"
|
|
13
13
|
},
|
|
@@ -2934,6 +2934,193 @@ function createResponsesWebSocketFetch(wsUrl, log) {
|
|
|
2934
2934
|
};
|
|
2935
2935
|
}
|
|
2936
2936
|
|
|
2937
|
+
// src/gateway-protocol.ts
|
|
2938
|
+
import { createHash as createHash2 } from "crypto";
|
|
2939
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync6, renameSync as renameSync2, unlinkSync, writeFileSync as writeFileSync2 } from "fs";
|
|
2940
|
+
import { dirname as dirname2, join as join4 } from "path";
|
|
2941
|
+
var DUAL_PROVIDER_IDS = /* @__PURE__ */ new Set([
|
|
2942
|
+
"zen",
|
|
2943
|
+
"go",
|
|
2944
|
+
"opencode",
|
|
2945
|
+
"opencode-zen",
|
|
2946
|
+
"opencode-go",
|
|
2947
|
+
"openrouter",
|
|
2948
|
+
"commandcode",
|
|
2949
|
+
"command-code"
|
|
2950
|
+
]);
|
|
2951
|
+
function normalizedRoot(baseURL) {
|
|
2952
|
+
return baseURL.replace(/\/+$/, "").replace(/\/v1$/i, "");
|
|
2953
|
+
}
|
|
2954
|
+
function isKnownDualHost(providerId, baseURL) {
|
|
2955
|
+
if (!baseURL) return false;
|
|
2956
|
+
let parsed;
|
|
2957
|
+
try {
|
|
2958
|
+
parsed = new URL(baseURL);
|
|
2959
|
+
} catch {
|
|
2960
|
+
return false;
|
|
2961
|
+
}
|
|
2962
|
+
const host = parsed.hostname.toLowerCase();
|
|
2963
|
+
const path = parsed.pathname.toLowerCase();
|
|
2964
|
+
const id = providerId?.trim().toLowerCase();
|
|
2965
|
+
if (host === "opencode.ai" && path.includes("/zen")) return true;
|
|
2966
|
+
if (host === "openrouter.ai" && path.includes("/api")) return true;
|
|
2967
|
+
if (host === "commandcode.ai" || host.endsWith(".commandcode.ai")) {
|
|
2968
|
+
if (path.includes("/provider")) return true;
|
|
2969
|
+
}
|
|
2970
|
+
return id !== void 0 && DUAL_PROVIDER_IDS.has(id) && (host === "opencode.ai" || host === "openrouter.ai" || host === "commandcode.ai");
|
|
2971
|
+
}
|
|
2972
|
+
function isDualProtocolGateway(providerId, baseURL) {
|
|
2973
|
+
return isKnownDualHost(providerId, baseURL);
|
|
2974
|
+
}
|
|
2975
|
+
function resolveProtocolAlternative(input) {
|
|
2976
|
+
if (!isDualProtocolGateway(input.providerId, input.baseURL)) return null;
|
|
2977
|
+
if (!input.baseURL) return null;
|
|
2978
|
+
const root = normalizedRoot(input.baseURL);
|
|
2979
|
+
if (input.modelFormat === "openai") {
|
|
2980
|
+
return {
|
|
2981
|
+
modelFormat: "anthropic",
|
|
2982
|
+
npm: "@ai-sdk/anthropic",
|
|
2983
|
+
baseURL: root,
|
|
2984
|
+
upstreamUrl: `${root}/v1/messages`
|
|
2985
|
+
};
|
|
2986
|
+
}
|
|
2987
|
+
const openaiRoot = `${root}/v1`;
|
|
2988
|
+
const openrouter = input.providerId?.trim().toLowerCase() === "openrouter" || input.npm === "@openrouter/ai-sdk-provider" || root.includes("openrouter.ai");
|
|
2989
|
+
return {
|
|
2990
|
+
modelFormat: "openai",
|
|
2991
|
+
npm: openrouter ? "@openrouter/ai-sdk-provider" : "@ai-sdk/openai-compatible",
|
|
2992
|
+
baseURL: openaiRoot,
|
|
2993
|
+
upstreamUrl: `${openaiRoot}/chat/completions`
|
|
2994
|
+
};
|
|
2995
|
+
}
|
|
2996
|
+
function errorText(value) {
|
|
2997
|
+
if (typeof value === "string") return value;
|
|
2998
|
+
if (!value || typeof value !== "object") return "";
|
|
2999
|
+
const record = value;
|
|
3000
|
+
const pieces = [record.message, record.responseBody, record.code].filter((item) => typeof item === "string");
|
|
3001
|
+
return pieces.join(" ");
|
|
3002
|
+
}
|
|
3003
|
+
function findErrorField(error, field, depth = 0) {
|
|
3004
|
+
if (!error || typeof error !== "object" || depth > 3) return void 0;
|
|
3005
|
+
const record = error;
|
|
3006
|
+
if (record[field] !== void 0) return record[field];
|
|
3007
|
+
return findErrorField(record.cause, field, depth + 1) ?? findErrorField(record.lastError, field, depth + 1);
|
|
3008
|
+
}
|
|
3009
|
+
function classifyProtocolFailure(error) {
|
|
3010
|
+
const statusValue = findErrorField(error, "statusCode") ?? findErrorField(error, "status");
|
|
3011
|
+
const status = typeof statusValue === "number" ? statusValue : void 0;
|
|
3012
|
+
const text = errorText(error) + " " + errorText(findErrorField(error, "cause")) + " " + errorText(findErrorField(error, "responseBody"));
|
|
3013
|
+
const lower = text.toLowerCase();
|
|
3014
|
+
if (lower.includes("abort") || lower.includes("cancel")) {
|
|
3015
|
+
return { retryable: false, status, reason: "request cancelled" };
|
|
3016
|
+
}
|
|
3017
|
+
if (lower.includes("not supported for format")) {
|
|
3018
|
+
return { retryable: true, status, reason: "provider rejected the selected API format" };
|
|
3019
|
+
}
|
|
3020
|
+
if (status === 401 || status === 403 || status === 429 || /invalid (api )?key|authentication|quota|billing|not in plan|model_not_in_plan/.test(lower)) {
|
|
3021
|
+
return { retryable: false, status, reason: "authentication or quota failure" };
|
|
3022
|
+
}
|
|
3023
|
+
if (/context (length|window)|too many tokens|maximum context|invalid tool|tool schema|moderation/.test(lower)) {
|
|
3024
|
+
return { retryable: false, status, reason: "request content was rejected" };
|
|
3025
|
+
}
|
|
3026
|
+
const explicitProtocol = status === 405 || status === 415 || status !== void 0 && [400, 404, 422].includes(status) && /endpoint|method|protocol|messages|chat\/completions|chat completions|anthropic/.test(lower);
|
|
3027
|
+
if (explicitProtocol) {
|
|
3028
|
+
return { retryable: true, status, reason: "provider rejected the selected API format" };
|
|
3029
|
+
}
|
|
3030
|
+
if (status === 500) {
|
|
3031
|
+
return { retryable: true, status, reason: "gateway returned an early internal error" };
|
|
3032
|
+
}
|
|
3033
|
+
return { retryable: false, status, reason: "failure is not attributable to API format" };
|
|
3034
|
+
}
|
|
3035
|
+
var remembered = /* @__PURE__ */ new Map();
|
|
3036
|
+
var PREFERENCE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
3037
|
+
var FAILURE_COOLDOWN_MS = 5 * 60 * 1e3;
|
|
3038
|
+
var loadedPersistentPath = null;
|
|
3039
|
+
function persistentPath() {
|
|
3040
|
+
return join4(getAppHome(), "protocol-cache.json");
|
|
3041
|
+
}
|
|
3042
|
+
function loadPersistent(now = Date.now()) {
|
|
3043
|
+
const path = persistentPath();
|
|
3044
|
+
if (loadedPersistentPath === path) return;
|
|
3045
|
+
remembered.clear();
|
|
3046
|
+
loadedPersistentPath = path;
|
|
3047
|
+
try {
|
|
3048
|
+
if (!existsSync4(path)) return;
|
|
3049
|
+
const raw = JSON.parse(readFileSync6(path, "utf8"));
|
|
3050
|
+
for (const [key, entry] of Object.entries(raw)) {
|
|
3051
|
+
if (!entry || entry.protocol !== void 0 && entry.protocol !== "openai" && entry.protocol !== "anthropic") continue;
|
|
3052
|
+
if (typeof entry.expiresAt !== "number" || entry.expiresAt <= now) continue;
|
|
3053
|
+
if (entry.cooldownUntil !== void 0 && typeof entry.cooldownUntil !== "number") continue;
|
|
3054
|
+
remembered.set(key, entry);
|
|
3055
|
+
}
|
|
3056
|
+
} catch {
|
|
3057
|
+
}
|
|
3058
|
+
}
|
|
3059
|
+
function savePersistent() {
|
|
3060
|
+
const path = persistentPath();
|
|
3061
|
+
try {
|
|
3062
|
+
mkdirSync3(dirname2(path), { recursive: true, mode: 448 });
|
|
3063
|
+
const live = {};
|
|
3064
|
+
const now = Date.now();
|
|
3065
|
+
for (const [key, entry] of remembered) {
|
|
3066
|
+
if (entry.expiresAt > now) live[key] = entry;
|
|
3067
|
+
}
|
|
3068
|
+
const tempPath = `${path}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
3069
|
+
writeFileSync2(tempPath, `${JSON.stringify(live)}
|
|
3070
|
+
`, { mode: 384 });
|
|
3071
|
+
renameSync2(tempPath, path);
|
|
3072
|
+
} catch {
|
|
3073
|
+
}
|
|
3074
|
+
}
|
|
3075
|
+
function protocolCacheKey(parts) {
|
|
3076
|
+
const credentialFingerprint = createHash2("sha256").update(parts.apiKey ?? "").update("\0").update(JSON.stringify(Object.entries(parts.headers ?? {}).sort(([a], [b]) => a.localeCompare(b)))).digest("hex");
|
|
3077
|
+
return createHash2("sha256").update(JSON.stringify({
|
|
3078
|
+
providerId: parts.providerId ?? "",
|
|
3079
|
+
modelId: parts.modelId,
|
|
3080
|
+
protocol: parts.protocol ?? "",
|
|
3081
|
+
baseURL: parts.baseURL ?? "",
|
|
3082
|
+
alternativeURL: parts.alternativeURL ?? "",
|
|
3083
|
+
credentialFingerprint
|
|
3084
|
+
})).digest("hex");
|
|
3085
|
+
}
|
|
3086
|
+
function rememberedProtocol(key, now = Date.now()) {
|
|
3087
|
+
loadPersistent(now);
|
|
3088
|
+
const entry = remembered.get(key);
|
|
3089
|
+
if (!entry) return void 0;
|
|
3090
|
+
if (entry.expiresAt <= now) {
|
|
3091
|
+
remembered.delete(key);
|
|
3092
|
+
savePersistent();
|
|
3093
|
+
return void 0;
|
|
3094
|
+
}
|
|
3095
|
+
if (entry.cooldownUntil !== void 0 && entry.cooldownUntil > now) return void 0;
|
|
3096
|
+
if (!entry.protocol) return void 0;
|
|
3097
|
+
return entry.protocol;
|
|
3098
|
+
}
|
|
3099
|
+
function rememberProtocol(key, protocol, now = Date.now()) {
|
|
3100
|
+
loadPersistent(now);
|
|
3101
|
+
remembered.set(key, { protocol, expiresAt: now + PREFERENCE_TTL_MS });
|
|
3102
|
+
savePersistent();
|
|
3103
|
+
}
|
|
3104
|
+
function protocolCooldownActive(key, now = Date.now()) {
|
|
3105
|
+
loadPersistent(now);
|
|
3106
|
+
const entry = remembered.get(key);
|
|
3107
|
+
if (!entry) return false;
|
|
3108
|
+
if (entry.expiresAt <= now) {
|
|
3109
|
+
remembered.delete(key);
|
|
3110
|
+
savePersistent();
|
|
3111
|
+
return false;
|
|
3112
|
+
}
|
|
3113
|
+
return entry.cooldownUntil !== void 0 && entry.cooldownUntil > now;
|
|
3114
|
+
}
|
|
3115
|
+
function rememberProtocolFailure(key, now = Date.now()) {
|
|
3116
|
+
loadPersistent(now);
|
|
3117
|
+
remembered.set(key, {
|
|
3118
|
+
expiresAt: now + PREFERENCE_TTL_MS,
|
|
3119
|
+
cooldownUntil: now + FAILURE_COOLDOWN_MS
|
|
3120
|
+
});
|
|
3121
|
+
savePersistent();
|
|
3122
|
+
}
|
|
3123
|
+
|
|
2937
3124
|
// src/provider-factory.ts
|
|
2938
3125
|
var RESPONSES_ONLY_PREFIXES = [
|
|
2939
3126
|
"gpt-5-codex",
|
|
@@ -2971,6 +3158,32 @@ function maxToolsForNpm(npm) {
|
|
|
2971
3158
|
function resolveProviderNpm(npm) {
|
|
2972
3159
|
return npm === "venice-ai-sdk-provider" ? "@ai-sdk/openai-compatible" : npm;
|
|
2973
3160
|
}
|
|
3161
|
+
var OPENROUTER_BLIND_MAX_OUTPUT_TOKENS = 65536;
|
|
3162
|
+
function createOpenRouterFetch(fetchImpl = globalThis.fetch) {
|
|
3163
|
+
return async (input, init) => {
|
|
3164
|
+
if (!init || typeof init.body !== "string") {
|
|
3165
|
+
return fetchImpl(input, init);
|
|
3166
|
+
}
|
|
3167
|
+
let parsed;
|
|
3168
|
+
try {
|
|
3169
|
+
parsed = JSON.parse(init.body);
|
|
3170
|
+
} catch {
|
|
3171
|
+
return fetchImpl(input, init);
|
|
3172
|
+
}
|
|
3173
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
3174
|
+
return fetchImpl(input, init);
|
|
3175
|
+
}
|
|
3176
|
+
const body = parsed;
|
|
3177
|
+
if (body.max_tokens !== OPENROUTER_BLIND_MAX_OUTPUT_TOKENS) {
|
|
3178
|
+
return fetchImpl(input, init);
|
|
3179
|
+
}
|
|
3180
|
+
const { max_tokens: _blindCap, ...withoutBlindCap } = body;
|
|
3181
|
+
return fetchImpl(input, {
|
|
3182
|
+
...init,
|
|
3183
|
+
body: JSON.stringify(withoutBlindCap)
|
|
3184
|
+
});
|
|
3185
|
+
};
|
|
3186
|
+
}
|
|
2974
3187
|
function findCreateFactory(mod) {
|
|
2975
3188
|
for (const value of Object.values(mod)) {
|
|
2976
3189
|
if (typeof value === "function" && value.name.startsWith("create")) {
|
|
@@ -2999,7 +3212,7 @@ async function loadSdkProviderFactory(npm) {
|
|
|
2999
3212
|
}
|
|
3000
3213
|
return cached;
|
|
3001
3214
|
}
|
|
3002
|
-
async function
|
|
3215
|
+
async function createLanguageModelSingle(spec) {
|
|
3003
3216
|
const npm = resolveProviderNpm(spec.npm);
|
|
3004
3217
|
const { modelId, apiKey, baseURL } = spec;
|
|
3005
3218
|
if (npm === VERTEX_ANTHROPIC_NPM) {
|
|
@@ -3046,7 +3259,8 @@ async function createLanguageModel(spec) {
|
|
|
3046
3259
|
if (npm === "@ai-sdk/anthropic") {
|
|
3047
3260
|
const { createAnthropic } = await import("@ai-sdk/anthropic");
|
|
3048
3261
|
const root = baseURL?.replace(/\/v1\/?$/, "").replace(/\/$/, "");
|
|
3049
|
-
const
|
|
3262
|
+
const openRouterBearer = spec.providerId?.trim().toLowerCase() === "openrouter" || root?.includes("openrouter.ai") === true;
|
|
3263
|
+
const anthropicOptions = spec.authType === "oauth" || openRouterBearer ? {
|
|
3050
3264
|
authToken: apiKey,
|
|
3051
3265
|
...spec.providerId === "claude-code" ? {
|
|
3052
3266
|
headers: {
|
|
@@ -3091,7 +3305,12 @@ async function createLanguageModel(spec) {
|
|
|
3091
3305
|
})(modelId);
|
|
3092
3306
|
} else if (npm === "@openrouter/ai-sdk-provider") {
|
|
3093
3307
|
const { createOpenRouter } = await import("@openrouter/ai-sdk-provider");
|
|
3094
|
-
model = createOpenRouter({
|
|
3308
|
+
model = createOpenRouter({
|
|
3309
|
+
apiKey,
|
|
3310
|
+
baseURL,
|
|
3311
|
+
fetch: createOpenRouterFetch(),
|
|
3312
|
+
...spec.headers ? { headers: spec.headers } : {}
|
|
3313
|
+
})(modelId);
|
|
3095
3314
|
} else {
|
|
3096
3315
|
const create = await loadSdkProviderFactory(npm);
|
|
3097
3316
|
const provider = create({
|
|
@@ -3110,6 +3329,259 @@ async function createLanguageModel(spec) {
|
|
|
3110
3329
|
}
|
|
3111
3330
|
return model;
|
|
3112
3331
|
}
|
|
3332
|
+
function protocolForNpm(npm) {
|
|
3333
|
+
return npm === "@ai-sdk/anthropic" ? "anthropic" : "openai";
|
|
3334
|
+
}
|
|
3335
|
+
function isSemanticStreamPart(part) {
|
|
3336
|
+
if (!part || typeof part !== "object") return false;
|
|
3337
|
+
const type = part.type;
|
|
3338
|
+
return type === "text-start" || type === "text-delta" || type === "reasoning-start" || type === "reasoning-delta" || type === "tool-input-start" || type === "tool-input-delta" || type === "tool-input-end" || type === "tool-call" || type === "tool-result" || type === "tool-approval-request";
|
|
3339
|
+
}
|
|
3340
|
+
var MAX_PROTOCOL_PRELUDE_BYTES = 64 * 1024;
|
|
3341
|
+
var MAX_PROTOCOL_PRELUDE_EVENTS = 256;
|
|
3342
|
+
function streamPartBytes(part) {
|
|
3343
|
+
try {
|
|
3344
|
+
return JSON.stringify(part)?.length ?? 0;
|
|
3345
|
+
} catch {
|
|
3346
|
+
return 0;
|
|
3347
|
+
}
|
|
3348
|
+
}
|
|
3349
|
+
function observeAlternateStream(result, protocol, cacheKey) {
|
|
3350
|
+
const reader = result.stream.getReader();
|
|
3351
|
+
const stream = new ReadableStream({
|
|
3352
|
+
start: (controller) => {
|
|
3353
|
+
void (async () => {
|
|
3354
|
+
let semantic = false;
|
|
3355
|
+
let finish = false;
|
|
3356
|
+
try {
|
|
3357
|
+
while (true) {
|
|
3358
|
+
const next = await reader.read();
|
|
3359
|
+
if (next.done) break;
|
|
3360
|
+
semantic ||= isSemanticStreamPart(next.value);
|
|
3361
|
+
finish ||= Boolean(next.value && typeof next.value === "object" && next.value.type === "finish");
|
|
3362
|
+
controller.enqueue(next.value);
|
|
3363
|
+
}
|
|
3364
|
+
if (semantic && finish) rememberProtocol(cacheKey, protocol);
|
|
3365
|
+
else rememberProtocolFailure(cacheKey);
|
|
3366
|
+
controller.close();
|
|
3367
|
+
} catch (error) {
|
|
3368
|
+
rememberProtocolFailure(cacheKey);
|
|
3369
|
+
controller.error(error);
|
|
3370
|
+
} finally {
|
|
3371
|
+
reader.releaseLock();
|
|
3372
|
+
}
|
|
3373
|
+
})();
|
|
3374
|
+
},
|
|
3375
|
+
cancel: (reason) => reader.cancel(reason)
|
|
3376
|
+
});
|
|
3377
|
+
return { ...result, stream };
|
|
3378
|
+
}
|
|
3379
|
+
function createProtocolFallbackMiddleware(primary, alternate, primaryProtocol, alternateProtocol, cacheKey, onDebug) {
|
|
3380
|
+
const alternateModel = alternate;
|
|
3381
|
+
const note = (message) => {
|
|
3382
|
+
onDebug?.(`[protocol-fallback] ${message}`);
|
|
3383
|
+
};
|
|
3384
|
+
const attemptOrder = (runPrimary, runAlternate) => rememberedProtocol(cacheKey) === alternateProtocol ? { first: runAlternate, second: runPrimary, firstProtocol: alternateProtocol, secondProtocol: primaryProtocol } : { first: runPrimary, second: runAlternate, firstProtocol: primaryProtocol, secondProtocol: alternateProtocol };
|
|
3385
|
+
const surfacedError = (firstError, secondError) => classifyProtocolFailure(secondError).retryable ? firstError : secondError;
|
|
3386
|
+
return {
|
|
3387
|
+
specificationVersion: "v4",
|
|
3388
|
+
wrapGenerate: async ({ doGenerate, params }) => {
|
|
3389
|
+
if (protocolCooldownActive(cacheKey)) return doGenerate();
|
|
3390
|
+
const { first, second, firstProtocol, secondProtocol } = attemptOrder(
|
|
3391
|
+
doGenerate,
|
|
3392
|
+
() => alternateModel.doGenerate(params)
|
|
3393
|
+
);
|
|
3394
|
+
try {
|
|
3395
|
+
const result = await first();
|
|
3396
|
+
rememberProtocol(cacheKey, firstProtocol);
|
|
3397
|
+
return result;
|
|
3398
|
+
} catch (error) {
|
|
3399
|
+
const failure = classifyProtocolFailure(error);
|
|
3400
|
+
if (!failure.retryable) throw error;
|
|
3401
|
+
note(`${firstProtocol} failed (${failure.reason}${failure.status ? `, HTTP ${failure.status}` : ""}); retrying ${secondProtocol}`);
|
|
3402
|
+
let result;
|
|
3403
|
+
try {
|
|
3404
|
+
result = await second();
|
|
3405
|
+
} catch (alternateError) {
|
|
3406
|
+
const alternateFailure = classifyProtocolFailure(alternateError);
|
|
3407
|
+
note(`${secondProtocol} failed after fallback (${alternateFailure.reason}${alternateFailure.status ? `, HTTP ${alternateFailure.status}` : ""})`);
|
|
3408
|
+
rememberProtocolFailure(cacheKey);
|
|
3409
|
+
throw surfacedError(error, alternateError);
|
|
3410
|
+
}
|
|
3411
|
+
rememberProtocol(cacheKey, secondProtocol);
|
|
3412
|
+
return result;
|
|
3413
|
+
}
|
|
3414
|
+
},
|
|
3415
|
+
wrapStream: async ({ doStream, params }) => {
|
|
3416
|
+
if (protocolCooldownActive(cacheKey)) return doStream();
|
|
3417
|
+
const { first, second, firstProtocol, secondProtocol } = attemptOrder(
|
|
3418
|
+
doStream,
|
|
3419
|
+
() => alternateModel.doStream(params)
|
|
3420
|
+
);
|
|
3421
|
+
let primaryResult;
|
|
3422
|
+
try {
|
|
3423
|
+
primaryResult = await first();
|
|
3424
|
+
} catch (error) {
|
|
3425
|
+
const failure = classifyProtocolFailure(error);
|
|
3426
|
+
if (!failure.retryable) throw error;
|
|
3427
|
+
note(`${firstProtocol} stream failed (${failure.reason}${failure.status ? `, HTTP ${failure.status}` : ""}); retrying ${secondProtocol}`);
|
|
3428
|
+
let alternateResult;
|
|
3429
|
+
try {
|
|
3430
|
+
alternateResult = await second();
|
|
3431
|
+
} catch (alternateError) {
|
|
3432
|
+
const alternateFailure = classifyProtocolFailure(alternateError);
|
|
3433
|
+
note(`${secondProtocol} stream failed after fallback (${alternateFailure.reason}${alternateFailure.status ? `, HTTP ${alternateFailure.status}` : ""})`);
|
|
3434
|
+
rememberProtocolFailure(cacheKey);
|
|
3435
|
+
throw surfacedError(error, alternateError);
|
|
3436
|
+
}
|
|
3437
|
+
return observeAlternateStream(alternateResult, secondProtocol, cacheKey);
|
|
3438
|
+
}
|
|
3439
|
+
const primaryReader = primaryResult.stream.getReader();
|
|
3440
|
+
let switched = false;
|
|
3441
|
+
const stream = new ReadableStream({
|
|
3442
|
+
start: (controller) => {
|
|
3443
|
+
void (async () => {
|
|
3444
|
+
const buffered = [];
|
|
3445
|
+
let bufferedBytes = 0;
|
|
3446
|
+
let committed = false;
|
|
3447
|
+
let terminal = false;
|
|
3448
|
+
const pipeAlternate = async (cause, force = false) => {
|
|
3449
|
+
if (switched) {
|
|
3450
|
+
controller.error(cause);
|
|
3451
|
+
return;
|
|
3452
|
+
}
|
|
3453
|
+
const failure = classifyProtocolFailure(cause);
|
|
3454
|
+
if (!force && !failure.retryable) {
|
|
3455
|
+
controller.error(cause);
|
|
3456
|
+
return;
|
|
3457
|
+
}
|
|
3458
|
+
switched = true;
|
|
3459
|
+
note(`${firstProtocol} stream failed (${failure.reason}${failure.status ? `, HTTP ${failure.status}` : ""}); retrying ${secondProtocol}`);
|
|
3460
|
+
try {
|
|
3461
|
+
await primaryReader.cancel();
|
|
3462
|
+
} catch {
|
|
3463
|
+
}
|
|
3464
|
+
let alternateResult;
|
|
3465
|
+
try {
|
|
3466
|
+
alternateResult = await second();
|
|
3467
|
+
} catch (alternateError) {
|
|
3468
|
+
const alternateFailure = classifyProtocolFailure(alternateError);
|
|
3469
|
+
note(`${secondProtocol} stream failed after fallback (${alternateFailure.reason}${alternateFailure.status ? `, HTTP ${alternateFailure.status}` : ""})`);
|
|
3470
|
+
rememberProtocolFailure(cacheKey);
|
|
3471
|
+
throw surfacedError(cause, alternateError);
|
|
3472
|
+
}
|
|
3473
|
+
const reader = alternateResult.stream.getReader();
|
|
3474
|
+
let alternateSemantic = false;
|
|
3475
|
+
let alternateFinish = false;
|
|
3476
|
+
try {
|
|
3477
|
+
while (true) {
|
|
3478
|
+
const next = await reader.read();
|
|
3479
|
+
if (next.done) break;
|
|
3480
|
+
if (isSemanticStreamPart(next.value)) alternateSemantic = true;
|
|
3481
|
+
if (next.value && typeof next.value === "object" && next.value.type === "finish") {
|
|
3482
|
+
alternateFinish = true;
|
|
3483
|
+
}
|
|
3484
|
+
controller.enqueue(next.value);
|
|
3485
|
+
}
|
|
3486
|
+
if (alternateSemantic && alternateFinish) rememberProtocol(cacheKey, secondProtocol);
|
|
3487
|
+
else rememberProtocolFailure(cacheKey);
|
|
3488
|
+
controller.close();
|
|
3489
|
+
} finally {
|
|
3490
|
+
reader.releaseLock();
|
|
3491
|
+
}
|
|
3492
|
+
};
|
|
3493
|
+
try {
|
|
3494
|
+
while (true) {
|
|
3495
|
+
const next = await primaryReader.read();
|
|
3496
|
+
if (next.done) {
|
|
3497
|
+
if (!committed && terminal) {
|
|
3498
|
+
await pipeAlternate(new Error("provider returned an empty response"), true);
|
|
3499
|
+
} else if (committed && terminal) {
|
|
3500
|
+
rememberProtocol(cacheKey, firstProtocol);
|
|
3501
|
+
controller.close();
|
|
3502
|
+
} else {
|
|
3503
|
+
controller.close();
|
|
3504
|
+
}
|
|
3505
|
+
return;
|
|
3506
|
+
}
|
|
3507
|
+
const part = next.value;
|
|
3508
|
+
if (!committed && part && typeof part === "object" && part.type === "error") {
|
|
3509
|
+
await pipeAlternate(part.error ?? part);
|
|
3510
|
+
return;
|
|
3511
|
+
}
|
|
3512
|
+
if (!committed) {
|
|
3513
|
+
buffered.push(part);
|
|
3514
|
+
bufferedBytes += streamPartBytes(part);
|
|
3515
|
+
if (isSemanticStreamPart(part) || buffered.length >= MAX_PROTOCOL_PRELUDE_EVENTS || bufferedBytes >= MAX_PROTOCOL_PRELUDE_BYTES) {
|
|
3516
|
+
committed = true;
|
|
3517
|
+
for (const pending of buffered) controller.enqueue(pending);
|
|
3518
|
+
buffered.length = 0;
|
|
3519
|
+
bufferedBytes = 0;
|
|
3520
|
+
} else if (part && typeof part === "object" && part.type === "finish") {
|
|
3521
|
+
terminal = true;
|
|
3522
|
+
}
|
|
3523
|
+
} else {
|
|
3524
|
+
controller.enqueue(part);
|
|
3525
|
+
}
|
|
3526
|
+
}
|
|
3527
|
+
} catch (error) {
|
|
3528
|
+
if (!committed) await pipeAlternate(error);
|
|
3529
|
+
else controller.error(error);
|
|
3530
|
+
} finally {
|
|
3531
|
+
primaryReader.releaseLock();
|
|
3532
|
+
}
|
|
3533
|
+
})().catch((error) => controller.error(error));
|
|
3534
|
+
},
|
|
3535
|
+
cancel: (reason) => primaryReader.cancel(reason)
|
|
3536
|
+
});
|
|
3537
|
+
return { ...primaryResult, stream };
|
|
3538
|
+
}
|
|
3539
|
+
};
|
|
3540
|
+
}
|
|
3541
|
+
async function createLanguageModel(spec) {
|
|
3542
|
+
const npm = resolveProviderNpm(spec.npm);
|
|
3543
|
+
const primary = await createLanguageModelSingle(spec);
|
|
3544
|
+
if (spec.authType === "oauth" || !spec.baseURL) return primary;
|
|
3545
|
+
const primaryProtocol = protocolForNpm(npm);
|
|
3546
|
+
const alternative = resolveProtocolAlternative({
|
|
3547
|
+
providerId: spec.providerId,
|
|
3548
|
+
modelFormat: primaryProtocol,
|
|
3549
|
+
npm,
|
|
3550
|
+
baseURL: spec.baseURL
|
|
3551
|
+
});
|
|
3552
|
+
if (!alternative || !isDualProtocolGateway(spec.providerId, spec.baseURL)) return primary;
|
|
3553
|
+
const cacheKey = protocolCacheKey({
|
|
3554
|
+
providerId: spec.providerId,
|
|
3555
|
+
modelId: spec.modelId,
|
|
3556
|
+
protocol: primaryProtocol,
|
|
3557
|
+
baseURL: spec.baseURL,
|
|
3558
|
+
alternativeURL: alternative.upstreamUrl,
|
|
3559
|
+
apiKey: spec.apiKey,
|
|
3560
|
+
headers: spec.headers
|
|
3561
|
+
});
|
|
3562
|
+
let alternate;
|
|
3563
|
+
try {
|
|
3564
|
+
alternate = await createLanguageModelSingle({
|
|
3565
|
+
...spec,
|
|
3566
|
+
npm: alternative.npm,
|
|
3567
|
+
baseURL: alternative.baseURL
|
|
3568
|
+
});
|
|
3569
|
+
} catch (error) {
|
|
3570
|
+
spec.onDebug?.(`[protocol-fallback] alternate SDK unavailable: ${error instanceof Error ? error.message : String(error)}`);
|
|
3571
|
+
return primary;
|
|
3572
|
+
}
|
|
3573
|
+
return wrapLanguageModel({
|
|
3574
|
+
model: primary,
|
|
3575
|
+
middleware: createProtocolFallbackMiddleware(
|
|
3576
|
+
primary,
|
|
3577
|
+
alternate,
|
|
3578
|
+
primaryProtocol,
|
|
3579
|
+
alternative.modelFormat,
|
|
3580
|
+
cacheKey,
|
|
3581
|
+
spec.onDebug
|
|
3582
|
+
)
|
|
3583
|
+
});
|
|
3584
|
+
}
|
|
3113
3585
|
var ANTHROPIC_EFFORT_LEVELS = ["low", "medium", "high"];
|
|
3114
3586
|
var OPENAI_EFFORT_LEVELS = ["low", "medium", "high"];
|
|
3115
3587
|
var GEMINI_EFFORT_LEVELS = ["low", "medium", "high"];
|
|
@@ -4695,6 +5167,13 @@ export {
|
|
|
4695
5167
|
CLINE_PASS_CATALOG_URL,
|
|
4696
5168
|
CLINE_PASS_VALIDATION_URL,
|
|
4697
5169
|
CLINE_PASS_LEGACY_DEFAULT_CONTEXT_WINDOW,
|
|
5170
|
+
getAppHome,
|
|
5171
|
+
getLegacyAppHome,
|
|
5172
|
+
getConfigPath,
|
|
5173
|
+
getProvidersPath,
|
|
5174
|
+
getLogsPath,
|
|
5175
|
+
getVertexModelsPath,
|
|
5176
|
+
getLegacyConfPath,
|
|
4698
5177
|
modelPrefersResponsesApi,
|
|
4699
5178
|
isSdkMigratedNpm,
|
|
4700
5179
|
maxToolsForNpm,
|
|
@@ -4704,13 +5183,6 @@ export {
|
|
|
4704
5183
|
effortProviderOptions,
|
|
4705
5184
|
deepMergeProviderOptions,
|
|
4706
5185
|
thinkingProviderOptions,
|
|
4707
|
-
getAppHome,
|
|
4708
|
-
getLegacyAppHome,
|
|
4709
|
-
getConfigPath,
|
|
4710
|
-
getProvidersPath,
|
|
4711
|
-
getLogsPath,
|
|
4712
|
-
getVertexModelsPath,
|
|
4713
|
-
getLegacyConfPath,
|
|
4714
5186
|
contextWindowFromHeuristics,
|
|
4715
5187
|
resolveContextWindow,
|
|
4716
5188
|
stripOneMContextSuffix,
|
|
@@ -4791,4 +5263,4 @@ export {
|
|
|
4791
5263
|
streamAnthropicResponse,
|
|
4792
5264
|
generateAnthropicResponse
|
|
4793
5265
|
};
|
|
4794
|
-
//# sourceMappingURL=chunk-
|
|
5266
|
+
//# sourceMappingURL=chunk-2LUDFIZX.js.map
|