@wengine-ai/llms 2.3.2414 → 2.3.2416
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 +2 -0
- package/THIRD_PARTY_NOTICES.md +3 -0
- package/dist/api/empty-answer-guard.d.ts +66 -0
- package/dist/api/plugin-request-state.d.ts +22 -0
- package/dist/api/plugin-response-error.d.ts +5 -0
- package/dist/api/routes.d.ts +2 -0
- package/dist/ccr/models.d.ts +9 -1
- package/dist/ccr/plugin-install-routes.d.ts +79 -0
- package/dist/cjs/server.cjs +219 -214
- package/dist/cjs/server.cjs.map +4 -4
- package/dist/esm/server.mjs +221 -216
- package/dist/esm/server.mjs.map +4 -4
- package/dist/index.html +41 -41
- package/dist/provider-plugins/host.d.ts +11 -0
- package/dist/provider-plugins/loader.d.ts +33 -0
- package/dist/provider-plugins/registry.d.ts +39 -0
- package/dist/provider-plugins/transformer-hook.d.ts +3 -0
- package/dist/provider-plugins/types.d.ts +236 -0
- package/dist/server.d.ts +8 -0
- package/dist/services/active-probe.d.ts +46 -4
- package/dist/services/provider-health.d.ts +18 -1
- package/dist/services/provider.d.ts +2 -0
- package/dist/services/proxy.d.ts +19 -1
- package/dist/services/transformer.d.ts +2 -0
- package/dist/transformer/openai.responses.transformer.d.ts +0 -6
- package/dist/types/llm.d.ts +4 -0
- package/package.json +2 -2
- package/tsconfig.tsbuildinfo +0 -1
package/README.md
CHANGED
|
@@ -93,6 +93,8 @@ npm install -g @wengine-ai/claude-code-router-next@latest && ccr restart
|
|
|
93
93
|
|
|
94
94
|
| 版本 | 发布内容 |
|
|
95
95
|
| --- | --- |
|
|
96
|
+
| **v2.3.2416** | <ul><li>**空回复现在会重试并降级**:上游返回 200 且 `content` 为空时不再被记成成功,改为触发同模型重试与 fallback;判据是 content 集合为空(tool_use-only 等合法回合不受影响),探测有字节与时间上限,慢速流不丢字节;Codex 侧同样拦住空 `response.completed`。</li><li>**流式回合不再记录 `output_tokens: 0`**:普通流式请求此前没有 usage 读取器,健康回复被记成全零且无 TTFT;现从 tee 分支采集并按流序合并。</li><li>**不健康 provider 从模型列表降级剔除**:熔断模型对所有 provider 类型生效隐藏,恢复后自动放回;订阅类 provider 新增两级后台探活(先查账号状态,再发真实最小请求),不再依赖真实请求反复撞死账号。</li></ul> |
|
|
97
|
+
| **v2.3.2415** | <ul><li>**新增 Qoder 订阅 provider(qoder.com + qoder.cn)**: 同一插件同引擎支持国际站与国内站 PAT 订阅,双 managed provider + 卡片双区块(中英双语);含整配置写串行化、401 单次刷新重放、每帧 1MiB 上限等健壮性修复。</li><li>**修复 WorkBuddy 6004 重置时间戳时区**: 测试改用 Asia/Shanghai 渲染,UTC CI 不再挂。</li><li>**修复 tool_result 图片按 base64 文本发送**: 图片改为视觉块发送,避免 200 万+ token 膨胀与上游拒绝。</li></ul> |
|
|
96
98
|
| **v2.3.2414** | <ul><li>**CCR Desktop(可选 Tauri 客户端)与 Pi 配置生命周期修复**: 新增可选 Tauri 桌面壳(托盘快速切换全局默认模型、quota 摘要、单实例;生命周期伴侣化、退出不杀 gateway);修复 `ccr restart` 的 disable→enable 与 legacy 项目 provider 改名迁移丢失 Pi 自定义模型字段(thinkingLevelMap/modelOverrides)。</li></ul> |
|
|
97
99
|
| **v2.3.2413** | <ul><li>**修复 TTFT 永久为空**: #65(pi 会话粘连)与 #67(temp-file 重写)合并冲突解决时丢失了 #65 的 \`statsKey\` 文件名优先逻辑,handler 用会话 UUID 命名临时文件而 reader 用 requestId 查找 → 永久不匹配 → 所有流式请求的 TTFT 都是 null、tokens_per_second 退化为按完整时长重算。修复后 handler 优先用 \`metadata.statsKey\` 命名文件(对齐 reader),TTFT 与真实速度恢复。</li></ul> |
|
|
98
100
|
| **v2.3.2412** | <ul><li>**修复用量/套餐接口在 node 版本不匹配时全部失效**: 当运行时 Node 的 ABI 与安装 better-sqlite3 时不同(如桌面应用注入自带 node),`ERR_DLOPEN_FAILED` 导致 `/api/usage*`、`/api/providers/quota` 全部 500,UI 用量页与套餐限额条消失。现在优先加载 better-sqlite3,加载失败自动回退 Node 内置 `node:sqlite`(Node ≥ 22.5),任意 node 版本下均可工作;CLI statusline 同步受益;回退时输出一次性显眼日志。</li><li>**修复 pi 模型条目自定义字段被刷新抹除**: `ensurePiCcrProvider` 此前整组替换 `models[]`,pi 的 `thinkingLevelMap` 等条目级自定义字段每次 CCR 启动/项目接管都被静默清掉(#69)。现在模型集合仍由 CCR 管理,但每个生成条目按同 id 与旧条目合并——受管字段以 CCR 为准,条目级自定义字段跨刷新保留。</li></ul> |
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detect an upstream answer that completed successfully (HTTP 200) but carried
|
|
3
|
+
* no content blocks at all.
|
|
4
|
+
*
|
|
5
|
+
* The client shows such a turn as "no response" — a message is sent and nothing
|
|
6
|
+
* happens. Because the wire response is an ordinary 200 with a normal
|
|
7
|
+
* message_start / message_delta / message_stop sequence, nothing in the request
|
|
8
|
+
* path throws, so neither the same-model retry nor the fallback chain runs, and
|
|
9
|
+
* the empty turn is recorded as a provider success (which also keeps the
|
|
10
|
+
* unhealthy model out of the fail pool).
|
|
11
|
+
*
|
|
12
|
+
* Judging the whole content-block set rather than the text keeps legitimate
|
|
13
|
+
* turns alive: a tool_use-only turn, a stop_sequence hit, and a refusal (which
|
|
14
|
+
* still emits a text block) all contain at least one content block.
|
|
15
|
+
*
|
|
16
|
+
* The probe is bounded by EMPTY_ANSWER_INSPECT_BYTES, so a healthy answer is
|
|
17
|
+
* released right after its first content block and is never buffered further.
|
|
18
|
+
*/
|
|
19
|
+
/** Upper bound on the bytes inspected before a stream is forwarded untouched. */
|
|
20
|
+
export declare const EMPTY_ANSWER_INSPECT_BYTES: number;
|
|
21
|
+
/**
|
|
22
|
+
* Upper bound on how long the probe may hold the response before forwarding.
|
|
23
|
+
*
|
|
24
|
+
* Long enough that a thinking model's first content block arrives well within it
|
|
25
|
+
* on a healthy stream, short enough that a stalled upstream cannot pin the
|
|
26
|
+
* request (and its connection) open.
|
|
27
|
+
*/
|
|
28
|
+
export declare const EMPTY_ANSWER_INSPECT_DEADLINE_MS = 10000;
|
|
29
|
+
/** `content` = real output; `empty` = finished with no block; `pending` = undecided. */
|
|
30
|
+
export type AnswerVerdict = "content" | "empty" | "pending";
|
|
31
|
+
/**
|
|
32
|
+
* Judge the buffered prefix of an answer.
|
|
33
|
+
*
|
|
34
|
+
* Line-prefix scanning (rather than JSON parsing) is deliberate: Anthropic SSE
|
|
35
|
+
* names each event on both `event:` and `data:` lines, and payloads may be split
|
|
36
|
+
* across reads, so a marker scan stays correct and cheap on every chunk.
|
|
37
|
+
*/
|
|
38
|
+
export declare function inspectAnswerPrefix(text: string): AnswerVerdict;
|
|
39
|
+
export interface AnswerProbeResult {
|
|
40
|
+
verdict: AnswerVerdict;
|
|
41
|
+
/** Chunks consumed while probing, in order, to be re-emitted verbatim. */
|
|
42
|
+
chunks: Uint8Array[];
|
|
43
|
+
/** Bytes consumed while probing. */
|
|
44
|
+
bytes: number;
|
|
45
|
+
/** Remaining upstream stream, or null when the answer ended / was cancelled. */
|
|
46
|
+
rest: ReadableStream<Uint8Array> | null;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Read enough of `body` to decide whether the answer carries any content.
|
|
50
|
+
*
|
|
51
|
+
* Consumed bytes are returned in `chunks` so a healthy stream can be rebuilt
|
|
52
|
+
* byte-for-byte. Reading stops at the first content block (fast, non-blocking
|
|
53
|
+
* for real answers), at the byte budget (forwarded untouched — an unknown shape
|
|
54
|
+
* is never second-guessed), at the terminal event (verdict `empty`), or when
|
|
55
|
+
* `deadlineMs` elapses.
|
|
56
|
+
*
|
|
57
|
+
* The deadline matters: before this guard, the first SSE event was forwarded as
|
|
58
|
+
* soon as it arrived. Waiting for a content block is open-ended by nature (a
|
|
59
|
+
* thinking model may emit nothing but heartbeats for a long time), so an
|
|
60
|
+
* upstream that stalls after `message_start` would hold the response — and the
|
|
61
|
+
* whole request — forever. On timeout the answer is treated as `pending` and
|
|
62
|
+
* forwarded as-is, which preserves the old behaviour for slow-but-alive streams.
|
|
63
|
+
*/
|
|
64
|
+
export declare function probeAnswer(body: ReadableStream<Uint8Array>, limitBytes?: number, deadlineMs?: number): Promise<AnswerProbeResult>;
|
|
65
|
+
/** Re-emit probed chunks, then continue with `rest`. */
|
|
66
|
+
export declare function rebuildAnswerStream(chunks: Uint8Array[], rest: ReadableStream<Uint8Array> | null): ReadableStream<Uint8Array>;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { ManagedRuntime } from "../provider-plugins/types";
|
|
2
|
+
interface PluginAttemptBudget {
|
|
3
|
+
refreshUsed: boolean;
|
|
4
|
+
terminalError?: Error;
|
|
5
|
+
}
|
|
6
|
+
interface PluginRequestState {
|
|
7
|
+
budgets: Map<string, PluginAttemptBudget>;
|
|
8
|
+
}
|
|
9
|
+
/** Install BEFORE any fallback {...req} copy: every copy shares this object. */
|
|
10
|
+
export declare function ensurePluginRequestState(req: any): PluginRequestState;
|
|
11
|
+
export declare function pluginAttemptBudget(req: any, runtime: ManagedRuntime, provider?: {
|
|
12
|
+
name?: string;
|
|
13
|
+
} | null): PluginAttemptBudget;
|
|
14
|
+
/**
|
|
15
|
+
* A classified nonretryable error stops this managed provider's models for the
|
|
16
|
+
* rest of the request — never an unrelated provider that happens to share the
|
|
17
|
+
* runtime (different subscription/credential pool).
|
|
18
|
+
*/
|
|
19
|
+
export declare function rememberPluginFailure(req: any, runtime: ManagedRuntime | undefined, error: any, provider?: {
|
|
20
|
+
name?: string;
|
|
21
|
+
} | null): boolean;
|
|
22
|
+
export {};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { ManagedRuntime } from "../provider-plugins/types";
|
|
2
|
+
/** No unclassified plugin response or read exception may reach rawBody/logging. */
|
|
3
|
+
export declare function pluginResponseError(runtime: ManagedRuntime, status: number, body: unknown, systemText: string, transport?: {
|
|
4
|
+
errorName?: string;
|
|
5
|
+
}): Promise<Error & Record<string, any>>;
|
package/dist/api/routes.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { FastifyInstance } from "fastify";
|
|
2
|
+
import { type PluginRegistry } from "../provider-plugins/registry";
|
|
2
3
|
import { ConfigService } from "../services/config";
|
|
3
4
|
import { ProviderService } from "../services/provider";
|
|
4
5
|
import { TransformerService } from "../services/transformer";
|
|
@@ -12,6 +13,7 @@ export declare const CCR_FAMILY_ALIAS: RegExp;
|
|
|
12
13
|
export declare function normalizeResponsesBody(body: any): void;
|
|
13
14
|
declare module "fastify" {
|
|
14
15
|
interface FastifyInstance {
|
|
16
|
+
pluginRegistry: PluginRegistry;
|
|
15
17
|
configService: ConfigService;
|
|
16
18
|
providerService: ProviderService;
|
|
17
19
|
transformerService: TransformerService;
|
package/dist/ccr/models.d.ts
CHANGED
|
@@ -23,4 +23,12 @@ export interface AnthropicModelInfo {
|
|
|
23
23
|
maxOutputTokens: number;
|
|
24
24
|
};
|
|
25
25
|
}
|
|
26
|
-
|
|
26
|
+
/**
|
|
27
|
+
* Predicate used to hide a provider/model pair from the model list.
|
|
28
|
+
*
|
|
29
|
+
* Pairs it returns true for are still routed (fallback, an explicit request for
|
|
30
|
+
* that exact model) but the client never sees them, so an unhealthy model stops
|
|
31
|
+
* being picked session after session.
|
|
32
|
+
*/
|
|
33
|
+
export type ModelAvailabilityFilter = (provider: string, model: string) => boolean;
|
|
34
|
+
export declare function listAnthropicCompatibleModels(config: any, isAvailable?: ModelAvailabilityFilter): AnthropicModelInfo[];
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider-plugin marketplace host routes (docs/design/provider-plugins.md §2.2).
|
|
3
|
+
*
|
|
4
|
+
* POST /api/plugins/install — one-click install from the npm registry using
|
|
5
|
+
* the SAME validated install core as the CLI (`ccr plugin add`): strict
|
|
6
|
+
* npm-name allowlist → npm pack (--ignore-scripts, bounded, throwaway
|
|
7
|
+
* credential env) → archive caps → payload validation → staged atomic swap →
|
|
8
|
+
* upsert plugins.json. Nothing hot-loads; callers restart via the existing
|
|
9
|
+
* POST /api/restart flow.
|
|
10
|
+
*
|
|
11
|
+
* GET /api/plugins/catalog — npm registry search for installable provider
|
|
12
|
+
* plugins (marker keyword), merged/deduped, with installed state. Registry
|
|
13
|
+
* failures degrade to an empty list + warning, never a 500.
|
|
14
|
+
*
|
|
15
|
+
* Both routes are LOCAL-ONLY (socket-address identity + loopback Origin/Host
|
|
16
|
+
* pinning, mirroring the workbuddy admin-route guard) on top of the normal
|
|
17
|
+
* CCR auth pipeline. Responses never include npm stderr, registry URLs or any
|
|
18
|
+
* credential material.
|
|
19
|
+
*/
|
|
20
|
+
import type { FastifyInstance } from "fastify";
|
|
21
|
+
import { type PluginInstallErrorCode } from "@wengine-ai/claude-code-router-shared";
|
|
22
|
+
/** npm search marker keywords: a package is only OFFERED when its
|
|
23
|
+
* package.json declares one of these (the search endpoint indexes exactly
|
|
24
|
+
* those package.json keywords). */
|
|
25
|
+
export declare const PLUGIN_MARKER_KEYWORDS: readonly ["ccr-provider-plugin", "claude-code-router-plugin"];
|
|
26
|
+
/** Default registry searched by GET /api/plugins/catalog. */
|
|
27
|
+
export declare const PLUGIN_CATALOG_REGISTRY = "https://registry.npmjs.org";
|
|
28
|
+
export interface CatalogEntry {
|
|
29
|
+
name: string;
|
|
30
|
+
description: string;
|
|
31
|
+
version: string;
|
|
32
|
+
installed: boolean;
|
|
33
|
+
installedVersion: string | null;
|
|
34
|
+
}
|
|
35
|
+
export interface CatalogResult {
|
|
36
|
+
ok: true;
|
|
37
|
+
plugins: CatalogEntry[];
|
|
38
|
+
/** Stable code when the registry could not be reached at all. */
|
|
39
|
+
warning?: "registry_unavailable";
|
|
40
|
+
}
|
|
41
|
+
/** Build the marketplace catalog: npm registry search by marker keyword,
|
|
42
|
+
* merged/deduped by package name, annotated with installed state. A total
|
|
43
|
+
* registry outage degrades to an empty list + warning — never a 500 and
|
|
44
|
+
* never raw upstream error text. */
|
|
45
|
+
export declare function buildPluginCatalog(configDir: string, options?: {
|
|
46
|
+
registryBase?: string;
|
|
47
|
+
timeoutMs?: number;
|
|
48
|
+
}): Promise<CatalogResult>;
|
|
49
|
+
export interface PluginMarketRouteDeps {
|
|
50
|
+
/** $CCR_CONFIG_DIR (the plugins/ root the loader reads). */
|
|
51
|
+
configDir: string;
|
|
52
|
+
/** Configured PORT fallback when the server is not listening (tests that
|
|
53
|
+
* inject() without a socket). */
|
|
54
|
+
configuredPort?: number;
|
|
55
|
+
/** Test seams. */
|
|
56
|
+
/** npm registry used by the install lane (defaults to npm's own registry).
|
|
57
|
+
* Also serves as the catalog base unless catalogRegistryBase is given. */
|
|
58
|
+
registry?: string;
|
|
59
|
+
npmTimeoutMs?: number;
|
|
60
|
+
/** Registry base for catalog search (tests stub this). */
|
|
61
|
+
catalogRegistryBase?: string;
|
|
62
|
+
/** Bounded catalog network wait (tests use small values). */
|
|
63
|
+
catalogTimeoutMs?: number;
|
|
64
|
+
}
|
|
65
|
+
/** HTTP status for a stable install error code. Only the contract-pinned
|
|
66
|
+
* mappings are load-bearing (corrupted → 409, bad name → 400); everything
|
|
67
|
+
* else follows "client fixable" vs "registry broke" vs "we broke". */
|
|
68
|
+
export declare function installErrorStatus(code: PluginInstallErrorCode | "install_failed"): number;
|
|
69
|
+
/**
|
|
70
|
+
* Local-only guard for the marketplace routes. The SOCKET address is the only
|
|
71
|
+
* trustworthy identity: a non-loopback caller is rejected regardless of any
|
|
72
|
+
* header it can spoof. For loopback callers an Origin header, when present,
|
|
73
|
+
* must match this server's own loopback authority, and a Host header must do
|
|
74
|
+
* the same — that is what stops a malicious local page (DNS rebinding / CSRF)
|
|
75
|
+
* from driving installs through the victim's own loopback socket.
|
|
76
|
+
* Originless loopback requests (local CLI/tooling) are allowed.
|
|
77
|
+
*/
|
|
78
|
+
export declare function assertLocalMarketplaceRequest(req: any, app: FastifyInstance, configuredPort: number): boolean;
|
|
79
|
+
export declare function registerPluginMarketRoutes(app: FastifyInstance, deps: PluginMarketRouteDeps): void;
|