Skip to content

Commit df736aa

Browse files
committed
对high-speed端点进行修复。添加了热更新config用例
1 parent 38bf39d commit df736aa

10 files changed

Lines changed: 542 additions & 33 deletions

‎config.example.yaml‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -199,8 +199,11 @@ clientSigning:
199199
# ignites a background draw loop (POST {origin}/api/v1/highspeed/draw at ~1/s —
200200
# the measured token-bucket refill rate; server next_draw_at is ignored on a
201201
# miss); on a hit, requests inside the card window are rewritten to the card
202-
# model with the highspeed auth set (X-Highspeed-Card-ID) and stay unsigned,
203-
# exactly like the official client. The loop self-extinguishes 5 minutes after
202+
# model with the highspeed auth set (X-Highspeed-Card-ID), rerouted to the
203+
# dedicated inference endpoint ({origin}/api/v1/highspeed/anthropic/v1/messages
204+
# — pinned by the official zcode-builtin provider rules, NOT the coding-plan
205+
# URL) and stay unsigned, exactly like the official client. The loop
206+
# self-extinguishes 5 minutes after
204207
# the last eligible request; account unavailability (503/3404) is
205208
# negative-cached 10min, HTTP 429 backs off 5s. task_id reuses the local
206209
# client-session affinity inference. Env override: ZCODE_HIGHSPEED_ENABLED=false.

‎src/highspeed/manager.test.ts‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { describe, expect, it, afterEach } from "bun:test";
22
import {
33
HighspeedManager,
44
applyHighspeedCardHeaders,
5+
highspeedLlmUrl,
56
isHighspeedCardRejection,
67
rewriteBodyModel,
78
type HighspeedAuth,
@@ -443,6 +444,21 @@ describe("highspeed helpers", () => {
443444
expect(rewriteBodyModel('{"messages":[]}', "x")).toBe('{"messages":[]}');
444445
});
445446

447+
it("highspeedLlmUrl pins the dedicated inference endpoint (zcode-builtin providerRules rev 31+)", () => {
448+
expect(highspeedLlmUrl("https://zcode.z.ai")).toBe("https://zcode.z.ai/api/v1/highspeed/anthropic/v1/messages");
449+
expect(highspeedLlmUrl("https://zcode.z.ai/")).toBe("https://zcode.z.ai/api/v1/highspeed/anthropic/v1/messages");
450+
});
451+
452+
it("manager.llmUrl derives from the draw-plane origin", () => {
453+
const manager = new HighspeedManager({
454+
origin: "https://hs.example.test",
455+
models: ["glm-5.3"],
456+
fetchImpl: (async () => new Response("{}")) as unknown as typeof fetch,
457+
});
458+
live.push(manager);
459+
expect(manager.llmUrl).toBe("https://hs.example.test/api/v1/highspeed/anthropic/v1/messages");
460+
});
461+
446462
it("applyHighspeedCardHeaders swaps the coding-plan dual auth and appends the card set", () => {
447463
const pairs: UpstreamHeaderPair[] = [
448464
["content-type", "application/json"],

‎src/highspeed/manager.ts‎

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import { randomUUID } from "node:crypto";
2828
import type { UpstreamHeaderPair } from "../proxy/upstream.js";
2929
import type { Credential } from "../auth/types.js";
3030
import { credentialString } from "../auth/types.js";
31+
import { warmHighspeedProviderConfig } from "./provider-config.js";
3132

3233
const DRAW_PATH = "/api/v1/highspeed/draw";
3334
/** Measured admitted-draw tail reaches ~5.6s (2026-10-14 probe); official transport timeout is 20s. */
@@ -200,6 +201,14 @@ export class HighspeedManager {
200201
return this.card;
201202
}
202203

204+
/**
205+
* Inference URL carded requests reroute to (`highspeedLlmUrl` of this
206+
* manager's origin — the same plane that serves /draw).
207+
*/
208+
get llmUrl(): string {
209+
return highspeedLlmUrl(this.origin);
210+
}
211+
203212
/**
204213
* Active card usable for THIS request model. A card carries the model it was
205214
* drawn for and mounting rewrites the request to card.model — so a flash
@@ -431,12 +440,17 @@ let defaultManagerKey = "";
431440
* Process-wide manager (single card slot, like the official host service).
432441
* Returns null when `highspeed.enabled` is false. Recreated when origin/models
433442
* change (Android `setConfig`) — the previous instance's loop is stopped.
443+
* Also warms the remote provider-config snapshot (client/configs → CDN →
444+
* highspeed baseUrl override; see `provider-config.ts`) — production path
445+
* only: tests injecting `opts.highspeed` bypass this and never touch the
446+
* network.
434447
*/
435448
export function getDefaultHighspeedManager(config: {
436449
highspeed: { enabled: boolean; origin: string; models: string[] };
437-
identity: { deviceMid?: string };
450+
identity: { deviceMid?: string; appVersion: string };
438451
}): HighspeedManager | null {
439452
if (!config.highspeed.enabled) return null;
453+
void warmHighspeedProviderConfig({ origin: config.highspeed.origin, appVersion: config.identity.appVersion });
440454
const key = `${config.highspeed.origin}\n${config.highspeed.models.join(",")}\n${config.identity.deviceMid}`;
441455
if (!defaultManager || key !== defaultManagerKey) {
442456
defaultManager?.stop();
@@ -466,6 +480,30 @@ export function rewriteBodyModel(body: string, model: string): string {
466480
}
467481
}
468482

483+
/**
484+
* Carded inference URL — the official client's `account:{family}-highspeed-card`
485+
* providers are pinned by `zcode-builtin.json` providerRules (rev 31=3.15.1
486+
* through rev 43, both zai and bigmodel families) to
487+
* `{origin}/api/v1/highspeed/anthropic`, and the anthropic SDK appends
488+
* `/v1/messages`. Carded requests therefore reroute here — NOT the coding-plan
489+
* provider URL (ultra after routing). Found 2026-11-09; the 2026-10-14
490+
* "挂卡头面仅三差异" audit missed this because the URL lives in the provider
491+
* config JSON, not the bundle code.
492+
*/
493+
export function highspeedLlmUrl(origin: string): string {
494+
return highspeedLlmUrlFromBase(`${origin.replace(/\/+$/u, "")}/api/v1/highspeed/anthropic`);
495+
}
496+
497+
/**
498+
* Full carded inference URL from a provider-rule baseUrl (the remote snapshot
499+
* in `provider-config.ts` may override the pinned default above; the official
500+
* anthropic SDK appends `/v1/messages` to whatever base the provider rules
501+
* carry, and so do we).
502+
*/
503+
export function highspeedLlmUrlFromBase(baseUrl: string): string {
504+
return `${baseUrl.replace(/\/+$/u, "")}/v1/messages`;
505+
}
506+
469507
/**
470508
* Apply the official highspeed auth shape to upstream header pairs
471509
* (`buildHighspeedTurnExecution` requestAuth): the coding-plan dual
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
/**
2+
* Remote provider-config resolver tests: pure extraction + the two-stage
3+
* fetch (client/configs → CDN) with TTL / dedup / fail-open semantics.
4+
* All HTTP goes through the test fetch seam — never the network.
5+
*/
6+
import { describe, expect, it, beforeEach, afterEach } from "bun:test";
7+
import {
8+
__resetHighspeedProviderConfigForTests,
9+
__setProviderConfigFetchForTests,
10+
extractHighspeedBaseUrls,
11+
resolvedHighspeedLlmUrl,
12+
warmHighspeedProviderConfig,
13+
} from "./provider-config.js";
14+
15+
function jsonResponse(body: unknown, status = 200): Response {
16+
return new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } });
17+
}
18+
19+
function builtinDoc(zai: string, bigmodel: string): unknown {
20+
return {
21+
schemaVersion: 2,
22+
revision: 43,
23+
config: {
24+
providerConfigRules: {
25+
providerRules: [
26+
{ providerId: "account:zai-individual-coding-plan", config: { api: { baseUrl: "https://api.z.ai/api/anthropic" } } },
27+
{ providerId: "account:zai-highspeed-card", config: { api: { baseUrl: zai } } },
28+
{ providerId: "account:bigmodel-highspeed-card", config: { api: { baseUrl: bigmodel } } },
29+
],
30+
},
31+
},
32+
};
33+
}
34+
35+
const CONFIGS_BODY = { code: 0, data: { configs: { builtin_provider_config_json: "https://cdn.example.test/builtin.json" } } };
36+
37+
/** Fetch impl routing client/configs → body, everything else (CDN) → doc. */
38+
function twoStageFetch(opts: { configs?: unknown; cdn?: unknown; configsStatus?: number } = {}) {
39+
const calls: string[] = [];
40+
const impl = (async (input: RequestInfo | URL) => {
41+
const url = String(input instanceof Request ? input.url : input);
42+
calls.push(url);
43+
if (url.includes("/api/v1/client/configs")) {
44+
return jsonResponse(opts.configs ?? CONFIGS_BODY, opts.configsStatus ?? 200);
45+
}
46+
return jsonResponse(opts.cdn ?? builtinDoc("https://cdn.example.test/hs/anthropic", "https://cdn.example.test/hs/anthropic"));
47+
}) as unknown as typeof fetch;
48+
return { impl, calls };
49+
}
50+
51+
const defaultNow = () => Date.now();
52+
53+
beforeEach(() => {
54+
__resetHighspeedProviderConfigForTests();
55+
__setProviderConfigFetchForTests(null);
56+
});
57+
58+
afterEach(() => {
59+
__resetHighspeedProviderConfigForTests();
60+
__setProviderConfigFetchForTests(null);
61+
});
62+
63+
describe("extractHighspeedBaseUrls", () => {
64+
it("extracts both families from a zcode-builtin shaped document", () => {
65+
const bases = extractHighspeedBaseUrls(builtinDoc("https://a.example.test/hs", "https://b.example.test/hs"));
66+
expect(bases.zai).toBe("https://a.example.test/hs");
67+
expect(bases.bigmodel).toBe("https://b.example.test/hs");
68+
});
69+
70+
it("returns {} for malformed shapes", () => {
71+
for (const bad of [null, 42, "x", {}, { config: {} }, { config: { providerConfigRules: {} } },
72+
{ config: { providerConfigRules: { providerRules: "nope" } } },
73+
// entries with non-string / blank baseUrl are skipped, not fatal
74+
{ config: { providerConfigRules: { providerRules: [
75+
{ providerId: "account:zai-highspeed-card", config: { api: { baseUrl: 7 } } },
76+
{ providerId: "account:bigmodel-highspeed-card", config: { api: { baseUrl: " " } } },
77+
] } } }]) {
78+
expect(extractHighspeedBaseUrls(bad)).toEqual({});
79+
}
80+
});
81+
82+
it("ignores unrelated provider ids and keeps a partial extraction", () => {
83+
const doc = { config: { providerConfigRules: { providerRules: [
84+
{ providerId: "account:zai-individual-coding-plan", config: { api: { baseUrl: "https://api.z.ai/api/anthropic" } } },
85+
{ providerId: "account:zai-highspeed-card", config: { api: { baseUrl: "https://hs.example.test/anthropic" } } },
86+
] } } };
87+
expect(extractHighspeedBaseUrls(doc)).toEqual({ zai: "https://hs.example.test/anthropic" });
88+
});
89+
});
90+
91+
describe("warmHighspeedProviderConfig + resolvedHighspeedLlmUrl", () => {
92+
it("resolves the CDN base and appends /v1/messages per family", async () => {
93+
const { impl, calls } = twoStageFetch({ cdn: builtinDoc("https://a.example.test/hs/anthropic/", "https://b.example.test/hs/anthropic") });
94+
__setProviderConfigFetchForTests(impl);
95+
await warmHighspeedProviderConfig({ origin: "https://zcode.z.ai", appVersion: "3.17.2", now: defaultNow });
96+
// two-stage: configs then CDN, with app_version/platform params
97+
expect(calls.length).toBe(2);
98+
expect(calls[0]).toContain("app_version=3.17.2");
99+
expect(calls[0]).toContain("platform=win32-x64");
100+
expect(calls[1]).toBe("https://cdn.example.test/builtin.json");
101+
// trailing slash on the base is trimmed before the SDK-style path join
102+
expect(resolvedHighspeedLlmUrl("zai")).toBe("https://a.example.test/hs/anthropic/v1/messages");
103+
expect(resolvedHighspeedLlmUrl("bigmodel")).toBe("https://b.example.test/hs/anthropic/v1/messages");
104+
});
105+
106+
it("serves the snapshot within TTL without refetching, refetches after expiry", async () => {
107+
let clock = 1_000_000;
108+
const { impl, calls } = twoStageFetch();
109+
__setProviderConfigFetchForTests(impl);
110+
await warmHighspeedProviderConfig({ origin: "https://zcode.z.ai", appVersion: "v", now: () => clock });
111+
expect(calls.length).toBe(2);
112+
await warmHighspeedProviderConfig({ origin: "https://zcode.z.ai", appVersion: "v", now: () => clock + 60_000 });
113+
expect(calls.length).toBe(2); // cached
114+
clock += 60 * 60 * 1000 + 1;
115+
await warmHighspeedProviderConfig({ origin: "https://zcode.z.ai", appVersion: "v", now: () => clock });
116+
expect(calls.length).toBe(4); // TTL expired → fresh two-stage fetch
117+
});
118+
119+
it("deduplicates concurrent warm calls into one in-flight fetch pair", async () => {
120+
const { impl, calls } = twoStageFetch();
121+
__setProviderConfigFetchForTests(impl);
122+
await Promise.all([
123+
warmHighspeedProviderConfig({ origin: "https://zcode.z.ai", appVersion: "v", now: defaultNow }),
124+
warmHighspeedProviderConfig({ origin: "https://zcode.z.ai", appVersion: "v", now: defaultNow }),
125+
]);
126+
expect(calls.length).toBe(2);
127+
});
128+
129+
it("fails open on configs error / missing CDN field / CDN error — default stays", async () => {
130+
for (const opts of [
131+
{ configsStatus: 500 }, // configs HTTP failure
132+
{ configs: { data: { configs: {} } } }, // no builtin_provider_config_json
133+
{ cdn: { nonsense: true } }, // CDN doc without our entries
134+
] as const) {
135+
__resetHighspeedProviderConfigForTests();
136+
const { impl } = twoStageFetch(opts as { configs?: unknown; cdn?: unknown; configsStatus?: number });
137+
__setProviderConfigFetchForTests(impl);
138+
await warmHighspeedProviderConfig({ origin: "https://zcode.z.ai", appVersion: "v", now: defaultNow });
139+
expect(resolvedHighspeedLlmUrl("zai")).toBeNull();
140+
expect(resolvedHighspeedLlmUrl("bigmodel")).toBeNull();
141+
}
142+
});
143+
144+
it("does not cache a document lacking our entries — the next warm refetches", async () => {
145+
let cdnBody: unknown = { config: { providerConfigRules: { providerRules: [] } } };
146+
const calls: string[] = [];
147+
__setProviderConfigFetchForTests((async (input: RequestInfo | URL) => {
148+
const url = String(input instanceof Request ? input.url : input);
149+
calls.push(url);
150+
return url.includes("/api/v1/client/configs")
151+
? jsonResponse(CONFIGS_BODY)
152+
: jsonResponse(cdnBody);
153+
}) as unknown as typeof fetch);
154+
await warmHighspeedProviderConfig({ origin: "https://zcode.z.ai", appVersion: "v", now: defaultNow });
155+
expect(calls.length).toBe(2);
156+
expect(resolvedHighspeedLlmUrl("zai")).toBeNull();
157+
cdnBody = builtinDoc("https://later.example.test/hs", "https://later.example.test/hs");
158+
await warmHighspeedProviderConfig({ origin: "https://zcode.z.ai", appVersion: "v", now: defaultNow });
159+
expect(calls.length).toBe(4); // nothing was cached → retried immediately
160+
expect(resolvedHighspeedLlmUrl("zai")).toBe("https://later.example.test/hs/v1/messages");
161+
});
162+
163+
it("never throws on network errors (warm resolves, default stays)", async () => {
164+
__setProviderConfigFetchForTests((async () => { throw new Error("boom"); }) as unknown as typeof fetch);
165+
await warmHighspeedProviderConfig({ origin: "https://zcode.z.ai", appVersion: "v", now: defaultNow });
166+
expect(resolvedHighspeedLlmUrl("zai")).toBeNull();
167+
});
168+
});

0 commit comments

Comments
 (0)