cctra 0.3.0

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.
Files changed (83) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +135 -0
  3. package/bin/cctra +2 -0
  4. package/bin/cctra-daemon.exe +0 -0
  5. package/bin/cctra.js +2 -0
  6. package/examples/plugins/oauth-internal.js +46 -0
  7. package/examples/plugins/openai-compatible.js +27 -0
  8. package/package.json +53 -0
  9. package/src/canonical/types.ts +132 -0
  10. package/src/commands/add.ts +159 -0
  11. package/src/commands/daemon.ts +102 -0
  12. package/src/commands/ls.ts +49 -0
  13. package/src/commands/model.ts +95 -0
  14. package/src/commands/plugin.ts +167 -0
  15. package/src/commands/rename.ts +33 -0
  16. package/src/commands/rm.ts +37 -0
  17. package/src/commands/serve.ts +29 -0
  18. package/src/commands/shared.ts +14 -0
  19. package/src/commands/show.ts +46 -0
  20. package/src/commands/tier.ts +91 -0
  21. package/src/convert/common/content-blocks.ts +29 -0
  22. package/src/convert/common/extras.ts +38 -0
  23. package/src/convert/common/reasoning.ts +19 -0
  24. package/src/convert/common/system-prompt.ts +15 -0
  25. package/src/convert/common/tool-calls.ts +29 -0
  26. package/src/convert/common/usage.ts +19 -0
  27. package/src/convert/inbound/anthropic-to-canonical.ts +106 -0
  28. package/src/convert/inbound/chat-to-canonical.ts +132 -0
  29. package/src/convert/inbound/responses-to-canonical.ts +92 -0
  30. package/src/convert/outbound/canonical-to-anthropic.ts +62 -0
  31. package/src/convert/outbound/canonical-to-chat.ts +101 -0
  32. package/src/convert/outbound/canonical-to-responses.ts +105 -0
  33. package/src/convert/streaming/inbound/anthropic-stream.ts +14 -0
  34. package/src/convert/streaming/inbound/chat-stream.ts +219 -0
  35. package/src/convert/streaming/inbound/pick.ts +21 -0
  36. package/src/convert/streaming/inbound/responses-stream.ts +276 -0
  37. package/src/convert/streaming/outbound/format-anthropic.ts +19 -0
  38. package/src/convert/streaming/outbound/format-chat.ts +133 -0
  39. package/src/convert/streaming/outbound/format-responses.ts +184 -0
  40. package/src/convert/upstream/canonical-to-anthropic.ts +111 -0
  41. package/src/convert/upstream/canonical-to-chat.ts +115 -0
  42. package/src/convert/upstream/canonical-to-responses.ts +123 -0
  43. package/src/core/config.ts +156 -0
  44. package/src/core/model-fetch.ts +124 -0
  45. package/src/core/resolve.ts +73 -0
  46. package/src/core/routing.ts +31 -0
  47. package/src/core/source.ts +28 -0
  48. package/src/daemon/install.ts +47 -0
  49. package/src/daemon/platform/linux.ts +65 -0
  50. package/src/daemon/platform/macos.ts +71 -0
  51. package/src/daemon/platform/windows.ts +70 -0
  52. package/src/daemon/start.ts +22 -0
  53. package/src/daemon/status.ts +19 -0
  54. package/src/daemon/stop.ts +58 -0
  55. package/src/index.ts +34 -0
  56. package/src/plugin/contract.ts +51 -0
  57. package/src/plugin/host.ts +27 -0
  58. package/src/plugin/loader.ts +55 -0
  59. package/src/plugin/sandbox.ts +3 -0
  60. package/src/providers/presets.ts +167 -0
  61. package/src/server/anthropic-parser.ts +44 -0
  62. package/src/server/cancelable-fetch.ts +21 -0
  63. package/src/server/chat-parser.ts +81 -0
  64. package/src/server/error-status.ts +18 -0
  65. package/src/server/error.ts +16 -0
  66. package/src/server/handlers/chat-completions.ts +94 -0
  67. package/src/server/handlers/messages.ts +89 -0
  68. package/src/server/handlers/models.ts +35 -0
  69. package/src/server/handlers/responses.ts +89 -0
  70. package/src/server/keepalive.ts +63 -0
  71. package/src/server/responses-parser.ts +62 -0
  72. package/src/server/serve.ts +79 -0
  73. package/src/server/sse.ts +61 -0
  74. package/src/server/upstream.ts +251 -0
  75. package/src/tier/builtin.ts +9 -0
  76. package/src/tier/resolve.ts +33 -0
  77. package/src/tier/store.ts +3 -0
  78. package/src/types.ts +94 -0
  79. package/src/ui/format.ts +44 -0
  80. package/src/ui/prompts.ts +34 -0
  81. package/src/utils/fuzzy.ts +48 -0
  82. package/src/utils/logger.ts +32 -0
  83. package/src/utils/paths.ts +48 -0
@@ -0,0 +1,49 @@
1
+ // ============================================================================
2
+ // cctra ls:列出所有 source
3
+ // ============================================================================
4
+ import { Command } from "commander";
5
+ import { withConfig } from "./shared";
6
+ import { green, dim, bold } from "../ui/format";
7
+ import { info } from "../ui/format";
8
+
9
+ export function registerLs(program: Command): void {
10
+ program
11
+ .command("ls")
12
+ .alias("list")
13
+ .description("List all subscriptions and plugins")
14
+ .action(() => {
15
+ withConfig((config) => {
16
+ const subs = Object.values(config.subscriptions);
17
+ const plugins = Object.values(config.plugins);
18
+
19
+ if (subs.length === 0 && plugins.length === 0) {
20
+ info("No subscriptions or plugins yet. Run `cctra add` to start.");
21
+ return;
22
+ }
23
+
24
+ if (subs.length > 0) {
25
+ console.log(bold("Subscriptions:"));
26
+ for (const [i, sub] of subs.entries()) {
27
+ const marker = green("*");
28
+ const index = dim(`${i + 1}.`);
29
+ const name = green(sub.name);
30
+ const vendorPart = sub.vendor ? ` ${dim(`[${sub.vendor}]`)}` : "";
31
+ const meta = ` ${dim(`(${sub.apiFormat}, ${sub.models.length} model${sub.models.length === 1 ? "" : "s"})`)}`;
32
+ console.log(`${marker} ${index} ${name}${vendorPart}${meta}`);
33
+ }
34
+ }
35
+
36
+ if (plugins.length > 0) {
37
+ if (subs.length > 0) console.log("");
38
+ console.log(bold("Plugins:"));
39
+ for (const [i, p] of plugins.entries()) {
40
+ const marker = p.enabled ? green("*") : " ";
41
+ const index = dim(`${i + 1}.`);
42
+ const name = p.enabled ? p.name : dim(p.name);
43
+ const meta = ` ${dim(`(${p.enabled ? "enabled" : "disabled"}, ${p.models.length} model${p.models.length === 1 ? "" : "s"})`)}`;
44
+ console.log(`${marker} ${index} ${name}${meta}`);
45
+ }
46
+ }
47
+ });
48
+ });
49
+ }
@@ -0,0 +1,95 @@
1
+ // ============================================================================
2
+ // cctra model <subcommand>:管理订阅的模型
3
+ // add / ls / rm / rename
4
+ // ============================================================================
5
+ import { Command } from "commander";
6
+ import * as p from "@clack/prompts";
7
+ import { withConfig } from "./shared";
8
+ import { checkCancel } from "../ui/prompts";
9
+ import { success, error as errorOut } from "../ui/format";
10
+
11
+ export function registerModel(program: Command): void {
12
+ const model = program.command("model").description("Manage models in a subscription");
13
+
14
+ // cctra model add <sub>
15
+ model
16
+ .command("add <sub>")
17
+ .description("Add a model to a subscription")
18
+ .action(async (sub: string) => {
19
+ try {
20
+ const id = checkCancel(await p.text({ message: "Model ID (upstream):", validate: (v) => !v?.trim() ? "Required" : undefined }));
21
+ const aliasRaw = checkCancel(await p.text({ message: "Alias (optional):", defaultValue: "" }));
22
+ const alias = aliasRaw.trim() || undefined;
23
+ withConfig((config) => {
24
+ if (!config.subscriptions[sub]) throw new Error(`Subscription "${sub}" not found.`);
25
+ const s = config.subscriptions[sub]!;
26
+ if (s.models.find((m) => m.id === id)) throw new Error(`Model "${id}" already exists.`);
27
+ s.models.push({ id, alias });
28
+ s.updatedAt = Date.now();
29
+ });
30
+ success(`Added model "${id}".`);
31
+ } catch (e) {
32
+ errorOut((e as Error).message);
33
+ process.exit(1);
34
+ }
35
+ });
36
+
37
+ // cctra model ls <sub>
38
+ model
39
+ .command("ls <sub>")
40
+ .description("List models in a subscription")
41
+ .action((sub: string) => {
42
+ withConfig((config) => {
43
+ if (!config.subscriptions[sub]) {
44
+ errorOut(`Subscription "${sub}" not found.`);
45
+ return;
46
+ }
47
+ for (const m of config.subscriptions[sub]!.models) {
48
+ const alias = m.alias ? ` (alias: ${m.alias})` : "";
49
+ console.log(`- ${m.id}${alias}`);
50
+ }
51
+ });
52
+ });
53
+
54
+ // cctra model rm <sub> <id-or-alias>
55
+ model
56
+ .command("rm <sub> <model>")
57
+ .description("Remove a model from a subscription")
58
+ .action((sub: string, model: string) => {
59
+ try {
60
+ withConfig((config) => {
61
+ if (!config.subscriptions[sub]) throw new Error(`Subscription "${sub}" not found.`);
62
+ const s = config.subscriptions[sub]!;
63
+ const idx = s.models.findIndex((m) => m.id === model || m.alias === model);
64
+ if (idx < 0) throw new Error(`Model "${model}" not found.`);
65
+ s.models.splice(idx, 1);
66
+ s.updatedAt = Date.now();
67
+ });
68
+ success(`Removed model "${model}".`);
69
+ } catch (e) {
70
+ errorOut((e as Error).message);
71
+ process.exit(1);
72
+ }
73
+ });
74
+
75
+ // cctra model rename <sub> <model> <new-alias>
76
+ model
77
+ .command("rename <sub> <model> <newAlias>")
78
+ .description("Set/change alias of a model")
79
+ .action((sub: string, model: string, newAlias: string) => {
80
+ try {
81
+ withConfig((config) => {
82
+ if (!config.subscriptions[sub]) throw new Error(`Subscription "${sub}" not found.`);
83
+ const s = config.subscriptions[sub]!;
84
+ const m = s.models.find((m) => m.id === model || m.alias === model);
85
+ if (!m) throw new Error(`Model "${model}" not found.`);
86
+ m.alias = newAlias.trim() || undefined;
87
+ s.updatedAt = Date.now();
88
+ });
89
+ success(`Updated alias.`);
90
+ } catch (e) {
91
+ errorOut((e as Error).message);
92
+ process.exit(1);
93
+ }
94
+ });
95
+ }
@@ -0,0 +1,167 @@
1
+ // ============================================================================
2
+ // cctra plugin <subcommand>:管理插件
3
+ // add / ls / show / enable / disable / rm
4
+ // ============================================================================
5
+ import { Command } from "commander";
6
+ import * as p from "@clack/prompts";
7
+ import { existsSync, statSync, readFileSync } from "node:fs";
8
+ import { createHash } from "node:crypto";
9
+ import { withConfig } from "./shared";
10
+ import { checkCancel } from "../ui/prompts";
11
+ import { success, error as errorOut, warn, info, dim } from "../ui/format";
12
+ import { addPlugin, updatePlugin, removePlugin } from "../core/config";
13
+ import { loadPlugin, clearPluginCache } from "../plugin/loader";
14
+ import type { PluginConfig } from "../types";
15
+
16
+ export function registerPlugin(program: Command): void {
17
+ const plugin = program.command("plugin").description("Manage local-path plugins");
18
+
19
+ // cctra plugin add <name> <path>
20
+ plugin
21
+ .command("add <name> <path>")
22
+ .description("Add a local-path plugin")
23
+ .action(async (name: string, path: string) => {
24
+ try {
25
+ // 验证文件存在
26
+ if (!existsSync(path)) {
27
+ errorOut(`File not found: ${path}`);
28
+ return;
29
+ }
30
+
31
+ // 计算 sha256
32
+ const content = readFileSync(path);
33
+ const sha256 = createHash("sha256").update(content).digest("hex");
34
+ const sizeKB = (statSync(path).size / 1024).toFixed(1);
35
+
36
+ // 警告 + 确认
37
+ warn("Plugin is arbitrary JavaScript. Loading gives it full process permissions.");
38
+ console.log(` ${dim("Path:")} ${path}`);
39
+ console.log(` ${dim("Size:")} ${sizeKB} KB`);
40
+ console.log(` ${dim("SHA-256:")} ${sha256}`);
41
+ const ok = checkCancel(
42
+ await p.confirm({ message: "Continue?", initialValue: false }),
43
+ );
44
+ if (!ok) return;
45
+
46
+ // 配置(这里先用空对象,v1 不做交互式 schema 推断)
47
+ const configRaw = checkCancel(
48
+ await p.text({ message: "Plugin config (JSON, optional):", defaultValue: "{}", validate: (v) => {
49
+ if (!v) return undefined;
50
+ try { JSON.parse(v); return undefined; } catch { return "Invalid JSON"; }
51
+ }}),
52
+ );
53
+ const config = JSON.parse(configRaw) as Record<string, unknown>;
54
+
55
+ // 尝试 import 一下确认合法
56
+ const tempPlugin: PluginConfig = {
57
+ kind: "plugin",
58
+ name,
59
+ path,
60
+ config,
61
+ enabled: true,
62
+ models: [],
63
+ };
64
+ const instance = await loadPlugin(tempPlugin, withConfig((c) => c));
65
+ if (!instance) {
66
+ errorOut(`Plugin failed to load. Check the file's syntax.`);
67
+ return;
68
+ }
69
+ // 拉模型列表
70
+ let models: { id: string; alias?: string }[] = [];
71
+ if (instance.listModels) {
72
+ try {
73
+ const { makePluginContext } = await import("../plugin/host");
74
+ const ctx = makePluginContext(name, config);
75
+ models = (await instance.listModels(ctx)).map((m) => ({ id: m.id, alias: m.alias }));
76
+ } catch (e) {
77
+ warn(`listModels() failed: ${(e as Error).message}`);
78
+ }
79
+ }
80
+ tempPlugin.models = models;
81
+
82
+ withConfig((c) => {
83
+ if (c.plugins[name]) updatePlugin(c, tempPlugin);
84
+ else addPlugin(c, tempPlugin);
85
+ });
86
+ success(`Added plugin "${name}" with ${models.length} model(s).`);
87
+ } catch (e) {
88
+ errorOut((e as Error).message);
89
+ process.exit(1);
90
+ }
91
+ });
92
+
93
+ // cctra plugin ls
94
+ plugin
95
+ .command("ls")
96
+ .description("List installed plugins")
97
+ .action(() => {
98
+ withConfig((config) => {
99
+ const plugins = Object.values(config.plugins);
100
+ if (plugins.length === 0) {
101
+ info("No plugins installed. Try `cctra plugin add <name> <path>`.");
102
+ return;
103
+ }
104
+ for (const p of plugins) {
105
+ const status = p.enabled ? "✓ enabled" : "✗ disabled";
106
+ console.log(`- ${p.name} ${dim(`(${p.models.length} model${p.models.length === 1 ? "" : "s"}, ${status})`)}`);
107
+ console.log(` ${dim("path:")} ${p.path}`);
108
+ }
109
+ });
110
+ });
111
+
112
+ // cctra plugin show <name>
113
+ plugin
114
+ .command("show <name>")
115
+ .description("Show plugin details")
116
+ .action((name: string) => {
117
+ withConfig((config) => {
118
+ const p = config.plugins[name];
119
+ if (!p) { errorOut(`Plugin "${name}" not found.`); return; }
120
+ console.log(`${p.name} ${dim(`(${p.enabled ? "enabled" : "disabled"})`)}`);
121
+ console.log(` ${dim("path:")} ${p.path}`);
122
+ console.log(` ${dim("config:")} ${JSON.stringify(p.config)}`);
123
+ console.log(` ${dim("models:")}`);
124
+ for (const m of p.models) {
125
+ const alias = m.alias ? ` (alias: ${m.alias})` : "";
126
+ console.log(` - ${m.id}${alias}`);
127
+ }
128
+ });
129
+ });
130
+
131
+ // cctra plugin enable / disable <name>
132
+ plugin
133
+ .command("enable <name>")
134
+ .description("Enable a plugin")
135
+ .action((name: string) => {
136
+ withConfig((config) => {
137
+ if (!config.plugins[name]) throw new Error(`Plugin "${name}" not found.`);
138
+ config.plugins[name]!.enabled = true;
139
+ });
140
+ clearPluginCache();
141
+ success(`Enabled "${name}".`);
142
+ });
143
+
144
+ plugin
145
+ .command("disable <name>")
146
+ .description("Disable a plugin")
147
+ .action((name: string) => {
148
+ withConfig((config) => {
149
+ if (!config.plugins[name]) throw new Error(`Plugin "${name}" not found.`);
150
+ config.plugins[name]!.enabled = false;
151
+ });
152
+ clearPluginCache();
153
+ success(`Disabled "${name}".`);
154
+ });
155
+
156
+ // cctra plugin rm <name>
157
+ plugin
158
+ .command("rm <name>")
159
+ .description("Remove a plugin (does not delete the .js file)")
160
+ .action((name: string) => {
161
+ withConfig((config) => {
162
+ if (!config.plugins[name]) throw new Error(`Plugin "${name}" not found.`);
163
+ removePlugin(config, name);
164
+ });
165
+ success(`Removed plugin "${name}".`);
166
+ });
167
+ }
@@ -0,0 +1,33 @@
1
+ // ============================================================================
2
+ // cctra rename <old> <new>:重命名订阅
3
+ // ============================================================================
4
+ import { Command } from "commander";
5
+ import { withConfig } from "./shared";
6
+ import { success, error as errorOut } from "../ui/format";
7
+
8
+ export function registerRename(program: Command): void {
9
+ program
10
+ .command("rename <old> <new>")
11
+ .description("Rename a subscription")
12
+ .action((oldName: string, newName: string) => {
13
+ try {
14
+ withConfig((config) => {
15
+ const normalized = newName.trim().toLowerCase();
16
+ if (!/^[a-z0-9][a-z0-9-]*$/.test(normalized)) {
17
+ throw new Error('New name must be kebab-case: lowercase letters, digits, hyphens.');
18
+ }
19
+ if (!config.subscriptions[oldName]) throw new Error(`Subscription "${oldName}" not found.`);
20
+ if (config.subscriptions[normalized]) throw new Error(`"${normalized}" already exists.`);
21
+ const sub = config.subscriptions[oldName]!;
22
+ sub.name = normalized;
23
+ sub.updatedAt = Date.now();
24
+ config.subscriptions[normalized] = sub;
25
+ delete config.subscriptions[oldName];
26
+ });
27
+ success(`Renamed to "${newName}".`);
28
+ } catch (e) {
29
+ errorOut((e as Error).message);
30
+ process.exit(1);
31
+ }
32
+ });
33
+ }
@@ -0,0 +1,37 @@
1
+ // ============================================================================
2
+ // cctra rm <name>:删除订阅或插件
3
+ // ============================================================================
4
+ import { Command } from "commander";
5
+ import * as p from "@clack/prompts";
6
+ import { withConfig } from "./shared";
7
+ import { removeSubscription, removePlugin } from "../core/config";
8
+ import { checkCancel } from "../ui/prompts";
9
+ import { success, error as errorOut } from "../ui/format";
10
+
11
+ export function registerRm(program: Command): void {
12
+ program
13
+ .command("rm <name>")
14
+ .alias("remove")
15
+ .description("Remove a subscription or plugin")
16
+ .action(async (name: string) => {
17
+ try {
18
+ const ok = checkCancel(
19
+ await p.confirm({
20
+ message: `Delete "${name}"?`,
21
+ initialValue: false,
22
+ }),
23
+ );
24
+ if (!ok) return;
25
+
26
+ withConfig((config) => {
27
+ if (config.subscriptions[name]) removeSubscription(config, name);
28
+ else if (config.plugins[name]) removePlugin(config, name);
29
+ else throw new Error(`Not found: "${name}"`);
30
+ });
31
+ success(`Removed "${name}".`);
32
+ } catch (e) {
33
+ errorOut((e as Error).message);
34
+ process.exit(1);
35
+ }
36
+ });
37
+ }
@@ -0,0 +1,29 @@
1
+ // ============================================================================
2
+ // cctra serve [--port N]:前台跑 HTTP server
3
+ // ============================================================================
4
+ import { Command } from "commander";
5
+ import { startServer } from "../server/serve";
6
+
7
+ export function registerServe(program: Command): void {
8
+ program
9
+ .command("serve")
10
+ .description("Run the HTTP server in the foreground")
11
+ .option("-p, --port <port>", "Override port (default from config)")
12
+ .action(async (opts: { port?: string }) => {
13
+ const port = opts.port ? parseInt(opts.port, 10) : undefined;
14
+ // 启动信息由 server/serve.ts 里的 logger.info 负责(写 stderr + daemon.log),
15
+ // 不要再 console.log 一行 —— 终端会看到重复
16
+ const handle = startServer(port);
17
+ // 不退出
18
+ process.on("SIGINT", () => {
19
+ handle.stop();
20
+ process.exit(0);
21
+ });
22
+ process.on("SIGTERM", () => {
23
+ handle.stop();
24
+ process.exit(0);
25
+ });
26
+ // 永久 hang
27
+ await new Promise(() => {});
28
+ });
29
+ }
@@ -0,0 +1,14 @@
1
+ // ============================================================================
2
+ // 共享工具:读/写 config 的 helper
3
+ // ============================================================================
4
+ import { loadConfigFile, saveConfigFile } from "../core/config";
5
+ import { ensureCctraDir } from "../utils/paths";
6
+ import type { Config } from "../types";
7
+
8
+ export function withConfig<T>(fn: (config: Config) => T): T {
9
+ ensureCctraDir();
10
+ const config = loadConfigFile();
11
+ const result = fn(config);
12
+ saveConfigFile(config);
13
+ return result;
14
+ }
@@ -0,0 +1,46 @@
1
+ // ============================================================================
2
+ // cctra show <name>:显示订阅/插件详情
3
+ // ============================================================================
4
+ import { Command } from "commander";
5
+ import { withConfig } from "./shared";
6
+ import { maskToken } from "../ui/prompts";
7
+ import { bold, dim, info } from "../ui/format";
8
+ import { getSource, isSubscription, isPlugin } from "../core/source";
9
+
10
+ export function registerShow(program: Command): void {
11
+ program
12
+ .command("show <name>")
13
+ .description("Show details of a subscription or plugin")
14
+ .action((name: string) => {
15
+ withConfig((config) => {
16
+ const s = getSource(config, name);
17
+ if (!s) {
18
+ info(`Not found: "${name}"`);
19
+ return;
20
+ }
21
+
22
+ console.log(bold(`${s.name}`) + ` ${dim(`(${s.kind})`)}`);
23
+ if (isSubscription(s)) {
24
+ if (s.vendor) {
25
+ console.log(` ${dim("vendor:")} ${s.vendor}`);
26
+ }
27
+ console.log(` ${dim("endpoint:")} ${s.endpoint}`);
28
+ console.log(` ${dim("token:")} ${maskToken(s.token)}`);
29
+ console.log(` ${dim("format:")} ${s.apiFormat}`);
30
+ } else if (isPlugin(s)) {
31
+ console.log(` ${dim("path:")} ${s.path}`);
32
+ console.log(` ${dim("enabled:")} ${s.enabled}`);
33
+ console.log(` ${dim("config:")} ${JSON.stringify(s.config)}`);
34
+ }
35
+ console.log(` ${dim("models:")}`);
36
+ if (s.models.length === 0) {
37
+ console.log(` ${dim("(none)")}`);
38
+ } else {
39
+ for (const m of s.models) {
40
+ const alias = m.alias ? ` ${dim(`(alias: ${m.alias})`)}` : "";
41
+ console.log(` - ${m.id}${alias}`);
42
+ }
43
+ }
44
+ });
45
+ });
46
+ }
@@ -0,0 +1,91 @@
1
+ // ============================================================================
2
+ // cctra tier <subcommand>:管理 tier 映射
3
+ // set / ls / show / rm
4
+ // ============================================================================
5
+ import { Command } from "commander";
6
+ import { withConfig } from "./shared";
7
+ import { success, error as errorOut, info, dim, bold } from "../ui/format";
8
+ import { setTier, removeTier } from "../core/config";
9
+ import { BUILTIN_TIERS } from "../types";
10
+
11
+ export function registerTier(program: Command): void {
12
+ const tier = program.command("tier").description("Manage tier model mappings");
13
+
14
+ // cctra tier set <name> <target>
15
+ tier
16
+ .command("set <name> <target>")
17
+ .description("Set/update a tier mapping (e.g. `cctra tier set cctra-pro my-sub/model-x`)")
18
+ .action((name: string, target: string) => {
19
+ try {
20
+ withConfig((config) => {
21
+ setTier(config, { name, target, description: config.tiers[name]?.description });
22
+ });
23
+ success(`Mapped ${name} → ${target}.`);
24
+ } catch (e) {
25
+ errorOut((e as Error).message);
26
+ process.exit(1);
27
+ }
28
+ });
29
+
30
+ // cctra tier ls
31
+ tier
32
+ .command("ls")
33
+ .description("List all tier mappings")
34
+ .action(() => {
35
+ withConfig((config) => {
36
+ const all = Object.values(config.tiers);
37
+ const builtinSet = new Set<string>(BUILTIN_TIERS);
38
+ const builtin = all.filter((t) => builtinSet.has(t.name));
39
+ const custom = all.filter((t) => !builtinSet.has(t.name));
40
+
41
+ console.log(bold("Built-in tiers:"));
42
+ for (const t of builtin) {
43
+ const target = t.target || dim("(not mapped)");
44
+ console.log(` ${t.name.padEnd(15)} → ${target} ${dim(t.description ?? "")}`);
45
+ }
46
+
47
+ if (custom.length > 0) {
48
+ console.log("");
49
+ console.log(bold("Custom tiers:"));
50
+ for (const t of custom) {
51
+ const target = t.target || dim("(not mapped)");
52
+ console.log(` ${t.name.padEnd(15)} → ${target} ${dim(t.description ?? "")}`);
53
+ }
54
+ }
55
+
56
+ if (builtin.length + custom.length === 0) {
57
+ info("No tiers configured.");
58
+ }
59
+ });
60
+ });
61
+
62
+ // cctra tier show <name>
63
+ tier
64
+ .command("show <name>")
65
+ .description("Show a tier's current target")
66
+ .action((name: string) => {
67
+ withConfig((config) => {
68
+ const t = config.tiers[name];
69
+ if (!t) { errorOut(`Tier "${name}" not found.`); return; }
70
+ const target = t.target || dim("(not mapped)");
71
+ console.log(`${name} → ${target}`);
72
+ if (t.description) console.log(dim(t.description));
73
+ });
74
+ });
75
+
76
+ // cctra tier rm <name>
77
+ tier
78
+ .command("rm <name>")
79
+ .description("Unmap a tier (built-ins keep the name, just clear target)")
80
+ .action((name: string) => {
81
+ try {
82
+ withConfig((config) => {
83
+ removeTier(config, name);
84
+ });
85
+ success(`Unmapped ${name}.`);
86
+ } catch (e) {
87
+ errorOut((e as Error).message);
88
+ process.exit(1);
89
+ }
90
+ });
91
+ }
@@ -0,0 +1,29 @@
1
+ import type { CanonicalContentBlock } from "../../canonical/types";
2
+
3
+ // ============================================================================
4
+ // Content Block 工具函数
5
+ // ============================================================================
6
+
7
+ /** 把字符串或 block 数组统一成 block 数组(便于遍历) */
8
+ export function ensureBlocks(content: string | CanonicalContentBlock[]): CanonicalContentBlock[] {
9
+ if (typeof content === "string") return [{ type: "text", text: content }];
10
+ return content;
11
+ }
12
+
13
+ /** 提取所有文本块(用于 Anthropic 风格的 system prompt 简化) */
14
+ export function extractText(blocks: CanonicalContentBlock[]): string {
15
+ return blocks
16
+ .filter((b): b is { type: "text"; text: string } => b.type === "text")
17
+ .map((b) => b.text)
18
+ .join("");
19
+ }
20
+
21
+ /** 判断 block 是否是 tool_use */
22
+ export function isToolUse(b: CanonicalContentBlock): b is { type: "tool_use"; id: string; name: string; input: unknown } {
23
+ return b.type === "tool_use";
24
+ }
25
+
26
+ /** 判断 block 是否是 tool_result */
27
+ export function isToolResult(b: CanonicalContentBlock): b is { type: "tool_result"; toolUseId: string; content: string | CanonicalContentBlock[]; isError?: boolean } {
28
+ return b.type === "tool_result";
29
+ }
@@ -0,0 +1,38 @@
1
+ // ============================================================================
2
+ // Per-protocol extras helpers
3
+ // 在 inbound 时把已知字段剥离,剩余未识别字段塞进对应协议桶
4
+ // 在 outbound 时按目标协议把桶 spread 回对象
5
+ // ============================================================================
6
+ import type { ProtocolExtras } from "../../canonical/types";
7
+
8
+ /** 把 obj 中 knownKeys 之外的字段剥出来,组成对应协议的 extras 桶 */
9
+ export function splitKnownAndExtras<T extends Record<string, unknown>>(
10
+ obj: T,
11
+ knownKeys: ReadonlySet<keyof T>,
12
+ protocol: keyof ProtocolExtras,
13
+ ): { known: T; extras: ProtocolExtras } {
14
+ const known = {} as T;
15
+ const extrasBag: Record<string, unknown> = {};
16
+ for (const [k, v] of Object.entries(obj)) {
17
+ if (knownKeys.has(k as keyof T)) {
18
+ (known as Record<string, unknown>)[k] = v;
19
+ } else {
20
+ extrasBag[k] = v;
21
+ }
22
+ }
23
+ const extras: ProtocolExtras = {};
24
+ if (Object.keys(extrasBag).length > 0) {
25
+ extras[protocol] = extrasBag;
26
+ }
27
+ return { known, extras };
28
+ }
29
+
30
+ /** 把 extras 中目标协议桶的字段 spread 进 target;防御性:target 已知字段优先 */
31
+ export function mergeExtras<T extends Record<string, unknown>>(
32
+ target: T,
33
+ extras: ProtocolExtras | undefined,
34
+ protocol: keyof ProtocolExtras,
35
+ ): T {
36
+ if (!extras?.[protocol]) return target;
37
+ return { ...target, ...extras[protocol] } as T;
38
+ }
@@ -0,0 +1,19 @@
1
+ import type { CanonicalContentBlock } from "../../canonical/types";
2
+
3
+ // 处理 reasoning/thinking 块的辅助
4
+ // Anthropic 的 thinking block 有 signature,cc-switch 的 thinking_rectifier 模式下
5
+ // 我们只在 signature 存在时回传,避免浪费 token
6
+
7
+ export function stripThinkingContent(blocks: CanonicalContentBlock[]): CanonicalContentBlock[] {
8
+ return blocks.map((b) => {
9
+ if (b.type === "thinking") {
10
+ // 只保留 signature(如果有),去掉 thinking 内容
11
+ return { type: "thinking", thinking: "", signature: b.signature };
12
+ }
13
+ return b;
14
+ });
15
+ }
16
+
17
+ export function hasThinkingSignature(blocks: CanonicalContentBlock[]): boolean {
18
+ return blocks.some((b) => b.type === "thinking" && b.signature);
19
+ }
@@ -0,0 +1,15 @@
1
+ import type { CanonicalRequest, CanonicalContentBlock } from "../../canonical/types";
2
+ import { ensureBlocks, extractText } from "./content-blocks";
3
+
4
+ /** 把 Canonical 顶级 system 转成字符串(用于 OpenAI Chat 的 messages[0].role=system) */
5
+ export function systemToString(system: CanonicalRequest["system"]): string | undefined {
6
+ if (system === undefined) return undefined;
7
+ if (typeof system === "string") return system;
8
+ return extractText(ensureBlocks(system));
9
+ }
10
+
11
+ /** 把字符串 system 转成 Canonical 顶级 system(block 形式) */
12
+ export function stringToSystem(s: string | undefined): CanonicalContentBlock[] | undefined {
13
+ if (!s) return undefined;
14
+ return [{ type: "text", text: s }];
15
+ }