@armadra/agent 0.2.1 → 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.
- package/CHANGELOG.md +15 -0
- package/README.md +55 -11
- package/dist/agent/session-cache.js +5 -2
- package/dist/agent/session-state.js +2 -1
- package/dist/agent/session-sync.js +9 -2
- package/dist/agent/session.js +8 -7
- package/dist/ai/apis/openai-responses.js +2 -1
- package/dist/ai/cache/fingerprint.d.ts +1 -1
- package/dist/ai/cache/fingerprint.js +1 -1
- package/dist/ai/cache/reporting.js +2 -1
- package/dist/ai/providers/channels.d.ts +51 -0
- package/dist/ai/providers/channels.js +96 -0
- package/dist/ai/providers/enrich.d.ts +34 -0
- package/dist/ai/providers/enrich.js +86 -0
- package/dist/ai/providers/models-dev-cache.d.ts +53 -0
- package/dist/ai/providers/models-dev-cache.js +147 -0
- package/dist/ai/providers/models-dev.d.ts +99 -0
- package/dist/ai/providers/models-dev.js +315 -0
- package/dist/ai/providers/registry.d.ts +35 -4
- package/dist/ai/providers/registry.js +177 -24
- package/dist/ai/types.d.ts +21 -1
- package/dist/bundle/ama.cjs +4075 -2008
- package/dist/cli/args.d.ts +6 -3
- package/dist/cli/args.js +30 -3
- package/dist/cli/bootstrap.js +1 -0
- package/dist/cli/compose-providers.js +3 -0
- package/dist/cli/compose-session.d.ts +3 -0
- package/dist/cli/compose.d.ts +3 -1
- package/dist/cli/compose.js +24 -9
- package/dist/cli/deps.d.ts +2 -0
- package/dist/cli/main.d.ts +2 -1
- package/dist/cli/main.js +13 -1
- package/dist/cli/startup-steps.js +6 -1
- package/dist/cli/subcommands/config.d.ts +11 -2
- package/dist/cli/subcommands/config.js +80 -4
- package/dist/cli/subcommands/context.js +1 -0
- package/dist/cli/subcommands/init.d.ts +6 -0
- package/dist/cli/subcommands/init.js +22 -0
- package/dist/cli/subcommands/model-meta.d.ts +16 -0
- package/dist/cli/subcommands/model-meta.js +54 -0
- package/dist/cli/subcommands/models-cache-probe.js +1 -1
- package/dist/cli/subcommands/models-discover.d.ts +6 -0
- package/dist/cli/subcommands/models-discover.js +37 -9
- package/dist/cli/subcommands/models.js +26 -18
- package/dist/cli/subcommands/providers-list.d.ts +8 -0
- package/dist/cli/subcommands/providers-list.js +97 -0
- package/dist/cli/subcommands/providers-plan.d.ts +79 -0
- package/dist/cli/subcommands/providers-plan.js +215 -0
- package/dist/cli/subcommands/providers.d.ts +27 -0
- package/dist/cli/subcommands/providers.js +433 -0
- package/dist/config/init.d.ts +30 -0
- package/dist/config/init.js +92 -0
- package/dist/config/json-schema.d.ts +14 -0
- package/dist/config/json-schema.js +182 -0
- package/dist/config/schema.d.ts +1 -1
- package/dist/config/schema.js +74 -11
- package/dist/config/types.d.ts +23 -5
- package/dist/config/types.js +2 -0
- package/dist/modes/image-input.d.ts +27 -0
- package/dist/modes/image-input.js +78 -0
- package/dist/modes/interactive/commands.js +2 -3
- package/dist/modes/interactive/interactive-mode.js +5 -1
- package/dist/modes/interactive/line/line-mode.js +3 -1
- package/dist/modes/interactive/startup-ui.d.ts +7 -2
- package/dist/modes/interactive/startup-ui.js +41 -11
- package/dist/modes/print/print-mode.d.ts +2 -0
- package/dist/modes/print/print-mode.js +12 -1
- package/dist/modes/rpc/commands.js +7 -2
- package/dist/rpc.d.ts +2 -0
- package/dist/session/projection.js +5 -1
- package/dist/session/types.d.ts +2 -0
- package/dist/tools/image-file.d.ts +31 -0
- package/dist/tools/image-file.js +114 -0
- package/dist/tools/read.d.ts +4 -8
- package/dist/tools/read.js +15 -64
- package/docs/providers.md +216 -1
- package/docs/rpc.md +3 -3
- package/docs/session-format.md +1 -1
- package/docs/tui.md +2 -0
- package/package.json +1 -1
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
* print 模式(`ama -p`,设计 §11.1 第 15–16 步)。[B6]
|
|
3
3
|
*
|
|
4
4
|
* - 提示 = 位置参数 + stdin 管道(stdin 非 TTY 时读到 EOF),两者都有时空一行拼接;都没有 → 用法错误 2。
|
|
5
|
+
* - 图片:`--image`(可重复)与提示里的 `@图片路径` / 图片文件路径作为附件(modes/image-input.ts);
|
|
6
|
+
* 显式附件遇到当前模型不收图片、文件不存在或超限 → 用法错误 2,不发请求。
|
|
5
7
|
* - `--output-format text`(缺省):运行结束后输出最后一条助手文本;`json`:一个结果对象(文本、
|
|
6
8
|
* 停止原因、用量、缓存命中率、[W3-C2] `cache` 统计(同 `get_session_stats.cache`)、全部条目);
|
|
7
9
|
* `stream-json`:每个会话事件一行(线上形状同 RPC,含 `cache_miss` / `cache_warm` /
|
|
@@ -13,6 +15,7 @@ import { currentSession } from "../../cli/compose-session.js";
|
|
|
13
15
|
import { ExitCode } from "../../cli/exit-codes.js";
|
|
14
16
|
import { errorText, lastAssistant, onStdoutClosed, onTerminationSignals } from "../shared.js";
|
|
15
17
|
import { toJsonLine, toWireEvent } from "./json-event.js";
|
|
18
|
+
import { promptImages, sessionModel } from "../image-input.js";
|
|
16
19
|
export function joinPrompt(argument, piped) {
|
|
17
20
|
const parts = [argument ?? "", piped.replace(/\s+$/, "")].filter((p) => p.trim() !== "");
|
|
18
21
|
return parts.join("\n\n");
|
|
@@ -27,6 +30,14 @@ export async function runPrintMode(runtime, context) {
|
|
|
27
30
|
return ExitCode.Usage;
|
|
28
31
|
}
|
|
29
32
|
const session = currentSession(runtime);
|
|
33
|
+
let images;
|
|
34
|
+
try {
|
|
35
|
+
images = await promptImages(prompt, context.args.images, io.cwd, sessionModel(runtime.providers, session));
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
io.stderr(`ama: ${errorText(error)}\n`);
|
|
39
|
+
return ExitCode.Usage;
|
|
40
|
+
}
|
|
30
41
|
const unsubscribe = format === "stream-json"
|
|
31
42
|
? session.subscribe((event) => io.stdout(`${toJsonLine(toWireEvent(event))}\n`))
|
|
32
43
|
: () => undefined;
|
|
@@ -42,7 +53,7 @@ export async function runPrintMode(runtime, context) {
|
|
|
42
53
|
});
|
|
43
54
|
let failure;
|
|
44
55
|
try {
|
|
45
|
-
await session.prompt(prompt);
|
|
56
|
+
await session.prompt(prompt, images.length > 0 ? { images } : {});
|
|
46
57
|
}
|
|
47
58
|
catch (error) {
|
|
48
59
|
failure = errorText(error);
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* `permission_response` 按 requestId 作答;先到的回答暂存,等 broker 被问到时取用。
|
|
10
10
|
* - 密钥不离开进程:`get_available_models` 只回 `hasKey` 与 `keySource`。
|
|
11
11
|
*/
|
|
12
|
+
import { formatModelRef } from "../../ai/providers/channels.js";
|
|
12
13
|
import { AgentSessionImpl } from "../../agent/session.js";
|
|
13
14
|
import { getSupportedLevels } from "../../ai/thinking.js";
|
|
14
15
|
import { switchSession } from "../../cli/compose-session.js";
|
|
@@ -146,7 +147,11 @@ export const handlers = {
|
|
|
146
147
|
// [W3-C2] 含 `cache`(三态、最近 / 会话命中率、未命中、保温、余量;第三波 §1.10)。
|
|
147
148
|
get_session_stats: async (_p, ctx) => ctx.session().getStats(),
|
|
148
149
|
set_model: async (p, ctx) => {
|
|
149
|
-
await ctx.session().setModel(
|
|
150
|
+
await ctx.session().setModel(formatModelRef({
|
|
151
|
+
provider: p.provider,
|
|
152
|
+
id: p.modelId,
|
|
153
|
+
...(p.channel !== undefined ? { channel: p.channel } : {}),
|
|
154
|
+
}));
|
|
150
155
|
return { model: ctx.session().state.model };
|
|
151
156
|
},
|
|
152
157
|
get_available_models: async (_p, ctx) => ({ models: await availableModels(ctx.runtime) }),
|
|
@@ -156,7 +161,7 @@ export const handlers = {
|
|
|
156
161
|
},
|
|
157
162
|
get_available_thinking_levels: async (_p, ctx) => {
|
|
158
163
|
const ref = ctx.session().state.model;
|
|
159
|
-
const found = ref === undefined ? undefined : ctx.runtime.providers.findModel(
|
|
164
|
+
const found = ref === undefined ? undefined : ctx.runtime.providers.findModel(formatModelRef(ref));
|
|
160
165
|
return { levels: found?.ok === true ? getSupportedLevels(found.model) : ["off"] };
|
|
161
166
|
},
|
|
162
167
|
set_steering_mode: async (p, ctx) => {
|
package/dist/rpc.d.ts
CHANGED
|
@@ -45,9 +45,11 @@ export interface RpcCommandMap {
|
|
|
45
45
|
get_messages: NoParams;
|
|
46
46
|
get_last_assistant_text: NoParams;
|
|
47
47
|
get_session_stats: NoParams;
|
|
48
|
+
/** `channel`:多渠道供应商的渠道名(缺省走模型的首选渠道)。 */
|
|
48
49
|
set_model: {
|
|
49
50
|
provider: string;
|
|
50
51
|
modelId: string;
|
|
52
|
+
channel?: string;
|
|
51
53
|
};
|
|
52
54
|
get_available_models: NoParams;
|
|
53
55
|
set_thinking_level: {
|
|
@@ -172,7 +172,11 @@ export function buildContext(branch) {
|
|
|
172
172
|
let thinkingLevel;
|
|
173
173
|
for (const entry of branch) {
|
|
174
174
|
if (entry.type === "model_change")
|
|
175
|
-
model = {
|
|
175
|
+
model = {
|
|
176
|
+
provider: entry.provider,
|
|
177
|
+
id: entry.modelId,
|
|
178
|
+
...(entry.channel !== undefined ? { channel: entry.channel } : {}),
|
|
179
|
+
};
|
|
176
180
|
else if (entry.type === "thinking_level_change")
|
|
177
181
|
thinkingLevel = entry.thinkingLevel;
|
|
178
182
|
}
|
package/dist/session/types.d.ts
CHANGED
|
@@ -72,6 +72,8 @@ export interface ModelChangeEntry extends EntryBase {
|
|
|
72
72
|
type: "model_change";
|
|
73
73
|
provider: string;
|
|
74
74
|
modelId: string;
|
|
75
|
+
/** 所选渠道(多渠道供应商);没有时走模型的首选渠道。 */
|
|
76
|
+
channel?: string;
|
|
75
77
|
}
|
|
76
78
|
export interface ThinkingLevelChangeEntry extends EntryBase {
|
|
77
79
|
type: "thinking_level_change";
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 图片文件(`read` 工具、`ama -p --image`、界面里的 `@图片路径` 共用):MIME 检测、尺寸、大小上限。
|
|
3
|
+
*
|
|
4
|
+
* - MIME 以文件头为准(PNG / JPEG / GIF / WebP),文件头认不出时退回扩展名;
|
|
5
|
+
* - 单张上限 5 MB(各家图片上限里最小的 Anthropic);
|
|
6
|
+
* - 不解码像素,只读文件头取尺寸。
|
|
7
|
+
*/
|
|
8
|
+
import type { ImageBlock } from "../ai/types.js";
|
|
9
|
+
export declare const MAX_IMAGE_BYTES: number;
|
|
10
|
+
export declare const IMAGE_EXTENSIONS: Readonly<Record<string, string>>;
|
|
11
|
+
/** 按扩展名(不分大小写);不是图片返回 undefined。 */
|
|
12
|
+
export declare function imageMimeFromPath(path: string): string | undefined;
|
|
13
|
+
/** 按文件头识别;认不出返回 undefined。 */
|
|
14
|
+
export declare function sniffImageMime(buf: Buffer): string | undefined;
|
|
15
|
+
export interface ImageSize {
|
|
16
|
+
width: number;
|
|
17
|
+
height: number;
|
|
18
|
+
}
|
|
19
|
+
/** 只读文件头取尺寸;不认识返回 undefined。 */
|
|
20
|
+
export declare function imageSize(buf: Buffer, mimeType: string): ImageSize | undefined;
|
|
21
|
+
export interface LoadedImage {
|
|
22
|
+
block: ImageBlock;
|
|
23
|
+
path: string;
|
|
24
|
+
mimeType: string;
|
|
25
|
+
bytes: number;
|
|
26
|
+
size?: ImageSize;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* 读一张图片作为附件。文件不存在、不是图片、超过上限 → AmaError(`invalid_arguments`),文案给用户看。
|
|
30
|
+
*/
|
|
31
|
+
export declare function loadImageFile(path: string): Promise<LoadedImage>;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 图片文件(`read` 工具、`ama -p --image`、界面里的 `@图片路径` 共用):MIME 检测、尺寸、大小上限。
|
|
3
|
+
*
|
|
4
|
+
* - MIME 以文件头为准(PNG / JPEG / GIF / WebP),文件头认不出时退回扩展名;
|
|
5
|
+
* - 单张上限 5 MB(各家图片上限里最小的 Anthropic);
|
|
6
|
+
* - 不解码像素,只读文件头取尺寸。
|
|
7
|
+
*/
|
|
8
|
+
import { readFile, stat } from "node:fs/promises";
|
|
9
|
+
import { extname } from "node:path";
|
|
10
|
+
import { AmaError } from "../errors.js";
|
|
11
|
+
export const MAX_IMAGE_BYTES = 5 * 1024 * 1024;
|
|
12
|
+
export const IMAGE_EXTENSIONS = {
|
|
13
|
+
".png": "image/png",
|
|
14
|
+
".jpg": "image/jpeg",
|
|
15
|
+
".jpeg": "image/jpeg",
|
|
16
|
+
".gif": "image/gif",
|
|
17
|
+
".webp": "image/webp",
|
|
18
|
+
};
|
|
19
|
+
/** 按扩展名(不分大小写);不是图片返回 undefined。 */
|
|
20
|
+
export function imageMimeFromPath(path) {
|
|
21
|
+
return IMAGE_EXTENSIONS[extname(path).toLowerCase()];
|
|
22
|
+
}
|
|
23
|
+
/** 按文件头识别;认不出返回 undefined。 */
|
|
24
|
+
export function sniffImageMime(buf) {
|
|
25
|
+
if (buf.length >= 8 && buf.readUInt32BE(0) === 0x89504e47 && buf.readUInt32BE(4) === 0x0d0a1a0a)
|
|
26
|
+
return "image/png";
|
|
27
|
+
if (buf.length >= 3 && buf[0] === 0xff && buf[1] === 0xd8 && buf[2] === 0xff)
|
|
28
|
+
return "image/jpeg";
|
|
29
|
+
if (buf.length >= 6 && /^GIF8[79]a$/.test(buf.toString("ascii", 0, 6)))
|
|
30
|
+
return "image/gif";
|
|
31
|
+
if (buf.length >= 12 &&
|
|
32
|
+
buf.toString("ascii", 0, 4) === "RIFF" &&
|
|
33
|
+
buf.toString("ascii", 8, 12) === "WEBP")
|
|
34
|
+
return "image/webp";
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
/** 只读文件头取尺寸;不认识返回 undefined。 */
|
|
38
|
+
export function imageSize(buf, mimeType) {
|
|
39
|
+
try {
|
|
40
|
+
if (mimeType === "image/png" && buf.length >= 24) {
|
|
41
|
+
return { width: buf.readUInt32BE(16), height: buf.readUInt32BE(20) };
|
|
42
|
+
}
|
|
43
|
+
if (mimeType === "image/gif" && buf.length >= 10) {
|
|
44
|
+
return { width: buf.readUInt16LE(6), height: buf.readUInt16LE(8) };
|
|
45
|
+
}
|
|
46
|
+
if (mimeType === "image/jpeg")
|
|
47
|
+
return jpegSize(buf);
|
|
48
|
+
if (mimeType === "image/webp")
|
|
49
|
+
return webpSize(buf);
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
function jpegSize(buf) {
|
|
57
|
+
let i = 2;
|
|
58
|
+
while (i + 9 < buf.length) {
|
|
59
|
+
if (buf[i] !== 0xff) {
|
|
60
|
+
i++;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
const marker = buf[i + 1] ?? 0;
|
|
64
|
+
const isSof = marker >= 0xc0 && marker <= 0xcf && ![0xc4, 0xc8, 0xcc].includes(marker);
|
|
65
|
+
if (isSof)
|
|
66
|
+
return { height: buf.readUInt16BE(i + 5), width: buf.readUInt16BE(i + 7) };
|
|
67
|
+
i += 2 + buf.readUInt16BE(i + 2);
|
|
68
|
+
}
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
function webpSize(buf) {
|
|
72
|
+
if (buf.length < 30 || buf.toString("ascii", 0, 4) !== "RIFF")
|
|
73
|
+
return undefined;
|
|
74
|
+
const chunk = buf.toString("ascii", 12, 16);
|
|
75
|
+
if (chunk === "VP8 ") {
|
|
76
|
+
return { width: buf.readUInt16LE(26) & 0x3fff, height: buf.readUInt16LE(28) & 0x3fff };
|
|
77
|
+
}
|
|
78
|
+
if (chunk === "VP8L") {
|
|
79
|
+
const b = buf.readUInt32LE(21);
|
|
80
|
+
return { width: (b & 0x3fff) + 1, height: ((b >> 14) & 0x3fff) + 1 };
|
|
81
|
+
}
|
|
82
|
+
if (chunk === "VP8X") {
|
|
83
|
+
return { width: buf.readUIntLE(24, 3) + 1, height: buf.readUIntLE(27, 3) + 1 };
|
|
84
|
+
}
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* 读一张图片作为附件。文件不存在、不是图片、超过上限 → AmaError(`invalid_arguments`),文案给用户看。
|
|
89
|
+
*/
|
|
90
|
+
export async function loadImageFile(path) {
|
|
91
|
+
let info;
|
|
92
|
+
try {
|
|
93
|
+
info = await stat(path);
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
throw new AmaError("invalid_arguments", `图片不存在:${path}`);
|
|
97
|
+
}
|
|
98
|
+
if (!info.isFile())
|
|
99
|
+
throw new AmaError("invalid_arguments", `不是文件:${path}`);
|
|
100
|
+
if (info.size > MAX_IMAGE_BYTES)
|
|
101
|
+
throw new AmaError("invalid_arguments", `图片超过 ${MAX_IMAGE_BYTES / 1024 / 1024} MB 上限:${path}(${(info.size / 1024 / 1024).toFixed(1)} MB)`);
|
|
102
|
+
const buf = await readFile(path);
|
|
103
|
+
const mimeType = sniffImageMime(buf) ?? imageMimeFromPath(path);
|
|
104
|
+
if (mimeType === undefined)
|
|
105
|
+
throw new AmaError("invalid_arguments", `不是支持的图片(PNG / JPEG / GIF / WebP):${path}`);
|
|
106
|
+
const size = imageSize(buf, mimeType);
|
|
107
|
+
return {
|
|
108
|
+
block: { type: "image", data: buf.toString("base64"), mimeType },
|
|
109
|
+
path,
|
|
110
|
+
mimeType,
|
|
111
|
+
bytes: buf.length,
|
|
112
|
+
...(size ? { size } : {}),
|
|
113
|
+
};
|
|
114
|
+
}
|
package/dist/tools/read.d.ts
CHANGED
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
* `read` 工具(设计 §5.2)。[B3]
|
|
3
3
|
*
|
|
4
4
|
* 文本按 `cat -n` 形状返回(行号右对齐 6 位 + Tab);头截断 2000 行 / 50 KB 先到者,末尾提示
|
|
5
|
-
* `offset` 续读;NUL 嗅探判二进制并拒绝;png / jpg / gif / webp 作为 ImageBlock
|
|
6
|
-
*
|
|
5
|
+
* `offset` 续读;NUL 嗅探判二进制并拒绝;png / jpg / gif / webp 作为 ImageBlock 返回(MIME 按文件头,
|
|
6
|
+
* 与 `--image` / `@图片` 共用 image-file.ts;模型不支持图片或超过 5 MB 时只给路径与尺寸);成功后
|
|
7
|
+
* `ctx.markRead(abs)`。
|
|
7
8
|
*/
|
|
8
9
|
import type { ToolContext, ToolDefinition, ToolResult } from "./types.js";
|
|
10
|
+
export { imageSize, type ImageSize } from "./image-file.js";
|
|
9
11
|
export interface ReadInput {
|
|
10
12
|
path: string;
|
|
11
13
|
offset?: number;
|
|
@@ -16,12 +18,6 @@ export interface ReadToolOptions {
|
|
|
16
18
|
supportsImages?(ctx: ToolContext): boolean;
|
|
17
19
|
}
|
|
18
20
|
export declare function isBinary(buf: Buffer): boolean;
|
|
19
|
-
export interface ImageSize {
|
|
20
|
-
width: number;
|
|
21
|
-
height: number;
|
|
22
|
-
}
|
|
23
|
-
/** 只读文件头取尺寸;不认识返回 undefined。 */
|
|
24
|
-
export declare function imageSize(buf: Buffer, mimeType: string): ImageSize | undefined;
|
|
25
21
|
/** `cat -n` 形状。 */
|
|
26
22
|
export declare function numberLines(lines: readonly string[], firstLine: number): string;
|
|
27
23
|
export declare function executeRead(input: ReadInput, ctx: ToolContext, options?: ReadToolOptions): Promise<ToolResult>;
|
package/dist/tools/read.js
CHANGED
|
@@ -2,21 +2,16 @@
|
|
|
2
2
|
* `read` 工具(设计 §5.2)。[B3]
|
|
3
3
|
*
|
|
4
4
|
* 文本按 `cat -n` 形状返回(行号右对齐 6 位 + Tab);头截断 2000 行 / 50 KB 先到者,末尾提示
|
|
5
|
-
* `offset` 续读;NUL 嗅探判二进制并拒绝;png / jpg / gif / webp 作为 ImageBlock
|
|
6
|
-
*
|
|
5
|
+
* `offset` 续读;NUL 嗅探判二进制并拒绝;png / jpg / gif / webp 作为 ImageBlock 返回(MIME 按文件头,
|
|
6
|
+
* 与 `--image` / `@图片` 共用 image-file.ts;模型不支持图片或超过 5 MB 时只给路径与尺寸);成功后
|
|
7
|
+
* `ctx.markRead(abs)`。
|
|
7
8
|
*/
|
|
8
9
|
import { readFile, stat } from "node:fs/promises";
|
|
9
|
-
import { extname } from "node:path";
|
|
10
10
|
import { displayPath, resolvePath } from "./paths.js";
|
|
11
11
|
import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, truncateHead } from "./truncate.js";
|
|
12
12
|
import { normalizeToLF, splitBom } from "./edit-fuzzy.js";
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
".jpg": "image/jpeg",
|
|
16
|
-
".jpeg": "image/jpeg",
|
|
17
|
-
".gif": "image/gif",
|
|
18
|
-
".webp": "image/webp",
|
|
19
|
-
};
|
|
13
|
+
import { MAX_IMAGE_BYTES, imageMimeFromPath, imageSize, sniffImageMime } from "./image-file.js";
|
|
14
|
+
export { imageSize } from "./image-file.js";
|
|
20
15
|
const SNIFF_BYTES = 8000;
|
|
21
16
|
export function isBinary(buf) {
|
|
22
17
|
const end = Math.min(buf.length, SNIFF_BYTES);
|
|
@@ -25,56 +20,6 @@ export function isBinary(buf) {
|
|
|
25
20
|
return true;
|
|
26
21
|
return false;
|
|
27
22
|
}
|
|
28
|
-
/** 只读文件头取尺寸;不认识返回 undefined。 */
|
|
29
|
-
export function imageSize(buf, mimeType) {
|
|
30
|
-
try {
|
|
31
|
-
if (mimeType === "image/png" && buf.length >= 24) {
|
|
32
|
-
return { width: buf.readUInt32BE(16), height: buf.readUInt32BE(20) };
|
|
33
|
-
}
|
|
34
|
-
if (mimeType === "image/gif" && buf.length >= 10) {
|
|
35
|
-
return { width: buf.readUInt16LE(6), height: buf.readUInt16LE(8) };
|
|
36
|
-
}
|
|
37
|
-
if (mimeType === "image/jpeg")
|
|
38
|
-
return jpegSize(buf);
|
|
39
|
-
if (mimeType === "image/webp")
|
|
40
|
-
return webpSize(buf);
|
|
41
|
-
}
|
|
42
|
-
catch {
|
|
43
|
-
return undefined;
|
|
44
|
-
}
|
|
45
|
-
return undefined;
|
|
46
|
-
}
|
|
47
|
-
function jpegSize(buf) {
|
|
48
|
-
let i = 2;
|
|
49
|
-
while (i + 9 < buf.length) {
|
|
50
|
-
if (buf[i] !== 0xff) {
|
|
51
|
-
i++;
|
|
52
|
-
continue;
|
|
53
|
-
}
|
|
54
|
-
const marker = buf[i + 1] ?? 0;
|
|
55
|
-
const isSof = marker >= 0xc0 && marker <= 0xcf && ![0xc4, 0xc8, 0xcc].includes(marker);
|
|
56
|
-
if (isSof)
|
|
57
|
-
return { height: buf.readUInt16BE(i + 5), width: buf.readUInt16BE(i + 7) };
|
|
58
|
-
i += 2 + buf.readUInt16BE(i + 2);
|
|
59
|
-
}
|
|
60
|
-
return undefined;
|
|
61
|
-
}
|
|
62
|
-
function webpSize(buf) {
|
|
63
|
-
if (buf.length < 30 || buf.toString("ascii", 0, 4) !== "RIFF")
|
|
64
|
-
return undefined;
|
|
65
|
-
const chunk = buf.toString("ascii", 12, 16);
|
|
66
|
-
if (chunk === "VP8 ") {
|
|
67
|
-
return { width: buf.readUInt16LE(26) & 0x3fff, height: buf.readUInt16LE(28) & 0x3fff };
|
|
68
|
-
}
|
|
69
|
-
if (chunk === "VP8L") {
|
|
70
|
-
const b = buf.readUInt32LE(21);
|
|
71
|
-
return { width: (b & 0x3fff) + 1, height: ((b >> 14) & 0x3fff) + 1 };
|
|
72
|
-
}
|
|
73
|
-
if (chunk === "VP8X") {
|
|
74
|
-
return { width: buf.readUIntLE(24, 3) + 1, height: buf.readUIntLE(27, 3) + 1 };
|
|
75
|
-
}
|
|
76
|
-
return undefined;
|
|
77
|
-
}
|
|
78
23
|
function error(message) {
|
|
79
24
|
return { content: message, isError: true };
|
|
80
25
|
}
|
|
@@ -82,8 +27,9 @@ function error(message) {
|
|
|
82
27
|
export function numberLines(lines, firstLine) {
|
|
83
28
|
return lines.map((line, i) => `${String(firstLine + i).padStart(6)}\t${line}`).join("\n");
|
|
84
29
|
}
|
|
85
|
-
async function readImage(abs, shown,
|
|
30
|
+
async function readImage(abs, shown, ctx, options) {
|
|
86
31
|
const buf = await readFile(abs);
|
|
32
|
+
const mimeType = sniffImageMime(buf) ?? imageMimeFromPath(abs) ?? "application/octet-stream";
|
|
87
33
|
const size = imageSize(buf, mimeType);
|
|
88
34
|
const dims = size ? `${size.width}x${size.height}, ` : "";
|
|
89
35
|
const caption = `Image: ${shown} (${dims}${mimeType}, ${formatSize(buf.length)})`;
|
|
@@ -92,6 +38,12 @@ async function readImage(abs, shown, mimeType, ctx, options) {
|
|
|
92
38
|
if (options.supportsImages && !options.supportsImages(ctx)) {
|
|
93
39
|
return { content: `${caption}\n[The current model does not accept image input.]`, details };
|
|
94
40
|
}
|
|
41
|
+
if (buf.length > MAX_IMAGE_BYTES) {
|
|
42
|
+
return {
|
|
43
|
+
content: `${caption}\n[Image exceeds the ${formatSize(MAX_IMAGE_BYTES)} attachment limit; not attached.]`,
|
|
44
|
+
details,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
95
47
|
const blocks = [
|
|
96
48
|
{ type: "text", text: caption },
|
|
97
49
|
{ type: "image", data: buf.toString("base64"), mimeType },
|
|
@@ -110,9 +62,8 @@ export async function executeRead(input, ctx, options = {}) {
|
|
|
110
62
|
}
|
|
111
63
|
if (info.isDirectory())
|
|
112
64
|
return error(`${shown} is a directory; use the ls tool instead`);
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
return readImage(abs, shown, mimeType, ctx, options);
|
|
65
|
+
if (imageMimeFromPath(abs) !== undefined)
|
|
66
|
+
return readImage(abs, shown, ctx, options);
|
|
116
67
|
const buf = await readFile(abs);
|
|
117
68
|
if (isBinary(buf))
|
|
118
69
|
return error(`${shown} appears to be a binary file; refusing to read it`);
|
package/docs/providers.md
CHANGED
|
@@ -2,6 +2,77 @@
|
|
|
2
2
|
|
|
3
3
|
内置供应商、模型引用、API Key、自定义供应商与中转站、各协议的 compat 开关,以及缓存。设计依据见 [design.md](design.md) §3、§9.1。
|
|
4
4
|
|
|
5
|
+
## 配置目录
|
|
6
|
+
|
|
7
|
+
缺省 `~/.config/ama/`(Windows `%APPDATA%\ama\`;`AMA_CONFIG_DIR` 优先,其次 `XDG_CONFIG_HOME/ama`)。数据
|
|
8
|
+
(会话、models.dev 缓存)在另一个目录:`~/.local/share/ama/`(`AMA_DATA_DIR` / `XDG_DATA_HOME`)。
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
~/.config/ama/ 0700
|
|
12
|
+
├── config.json 用户级配置:供应商、渠道、模型、权限、工具、缓存……(直接编辑)
|
|
13
|
+
├── config.schema.json config.json 的 JSON Schema(编辑器补全与校验;由 ama 生成,会被重写)
|
|
14
|
+
├── auth.json API key(0600;只在 ama auth set / ama providers add 时创建)
|
|
15
|
+
├── config.json.bak ama 改写 config.json 前的备份
|
|
16
|
+
├── hooks.json / trust.json / keybindings.json (按需)
|
|
17
|
+
└── skills/ prompts/ (按需)
|
|
18
|
+
~/.local/share/ama/
|
|
19
|
+
├── sessions/ 会话
|
|
20
|
+
└── models-dev.json models.dev 元数据缓存
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
- `ama init`:建目录(0700)并补齐缺失的 `config.json` 与 `config.schema.json`,逐个打印「已创建」或
|
|
24
|
+
「已存在,未改动」;已存在的 `config.json` 一律不覆盖(`--force` 也不),`config.schema.json` 不是用户文件,
|
|
25
|
+
每次 `init` 都重写为当前版本;不创建空的 `auth.json`。
|
|
26
|
+
- **首次运行自动初始化**:CLI 启动时若配置目录不存在,静默建目录并写最小 `config.json` 与 schema
|
|
27
|
+
(`AMA_NO_INIT=1` 关闭;SDK 与测试不触发)。
|
|
28
|
+
- 最小 `config.json`:
|
|
29
|
+
|
|
30
|
+
```json
|
|
31
|
+
{
|
|
32
|
+
"$schema": "./config.schema.json",
|
|
33
|
+
"version": 1,
|
|
34
|
+
"thinkingLevel": "medium",
|
|
35
|
+
"permission": { "mode": "default" },
|
|
36
|
+
"tools": { "preset": "default" },
|
|
37
|
+
"providers": {}
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
不写 `defaultModel`(零配置按「第一个有 key / 本地可达的供应商」挑选)。
|
|
42
|
+
|
|
43
|
+
- `ama config path`:打印配置目录、数据目录与各文件路径(标出是否存在);`ama config edit`:用
|
|
44
|
+
`$VISUAL` / `$EDITOR` 打开 `config.json`(不存在先 `init`),没有编辑器时打印路径。
|
|
45
|
+
|
|
46
|
+
示例:一个三渠道中转 + 一个图像模型 + 内置供应商的覆盖。
|
|
47
|
+
|
|
48
|
+
```json
|
|
49
|
+
{
|
|
50
|
+
"$schema": "./config.schema.json",
|
|
51
|
+
"version": 1,
|
|
52
|
+
"defaultModel": "packy/kimi-k2.5",
|
|
53
|
+
"providers": {
|
|
54
|
+
"packy": {
|
|
55
|
+
"apiKey": "$PACKY_API_KEY",
|
|
56
|
+
"channels": {
|
|
57
|
+
"chat": { "api": "openai-completions", "baseUrl": "https://www.packyapi.com/v1" },
|
|
58
|
+
"responses": { "api": "openai-responses", "baseUrl": "https://www.packyapi.com/v1" },
|
|
59
|
+
"messages": { "api": "anthropic-messages", "baseUrl": "https://www.packyapi.com" }
|
|
60
|
+
},
|
|
61
|
+
"models": [
|
|
62
|
+
{ "id": "kimi-k2.5", "channels": ["chat", "messages"] },
|
|
63
|
+
{ "id": "grok-4.7", "channels": ["responses"] },
|
|
64
|
+
{
|
|
65
|
+
"id": "qwen3-vl-flash",
|
|
66
|
+
"input": ["text", "image"],
|
|
67
|
+
"modelsDev": "llmgateway/qwen3-vl-flash"
|
|
68
|
+
}
|
|
69
|
+
]
|
|
70
|
+
},
|
|
71
|
+
"deepseek": { "modelOverrides": [{ "id": "deepseek-flash", "contextWindow": 131072 }] }
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
5
76
|
## 内置供应商
|
|
6
77
|
|
|
7
78
|
| id | 协议 | baseUrl | API Key 环境变量(顺序) |
|
|
@@ -95,7 +166,8 @@ export PACKY_API_KEY=sk-...
|
|
|
95
166
|
`--probe` 对每个模型依次试供应商协议、completions、responses、messages 的最小请求,记第一个成功
|
|
96
167
|
的(每模型最多 3 次,`--limit` 限制探测的模型数,缺省 30,执行前打印预估,401 / 403 / 429 即停);
|
|
97
168
|
`--write` 把结果合并进用户级 `config.json`(已有同 id 不覆盖,只写 `id` 与和供应商不同的 `api`,
|
|
98
|
-
原文件备份为 `config.json.bak
|
|
169
|
+
原文件备份为 `config.json.bak`)。上下文等元数据在运行时从 models.dev 缓存补(见下文「模型元数据」),
|
|
170
|
+
匹配不到的条目没有 `contextWindow`,自动压缩随之关闭,需要时手动补。
|
|
99
171
|
|
|
100
172
|
```sh
|
|
101
173
|
ama models discover packy --probe --write --limit 8
|
|
@@ -112,6 +184,149 @@ Claude Code 的通行约定),优先级低于 config 与 auth.json 的 `baseU
|
|
|
112
184
|
OPENAI_BASE_URL=https://proxy.example/v1 OPENAI_API_KEY=$PACKY_API_KEY ama -p "hi" --model openai/qwen3.8-flash
|
|
113
185
|
```
|
|
114
186
|
|
|
187
|
+
### 渠道(channels):一个供应商、多种接口
|
|
188
|
+
|
|
189
|
+
同一个中转常常同时开放 Chat Completions(`/v1/chat/completions`)、Responses(`/v1/responses`)与
|
|
190
|
+
Anthropic Messages(`/v1/messages`),且每个模型只在其中一部分接口上可用。**渠道**是「协议 + 地址(+ 可选
|
|
191
|
+
的 key / headers / compat)」,一个供应商可以有多个渠道,模型声明自己挂在哪些渠道上:
|
|
192
|
+
|
|
193
|
+
```json
|
|
194
|
+
{
|
|
195
|
+
"providers": {
|
|
196
|
+
"packy": {
|
|
197
|
+
"name": "Packy",
|
|
198
|
+
"apiKey": "$PACKY_API_KEY",
|
|
199
|
+
"channels": {
|
|
200
|
+
"chat": { "api": "openai-completions", "baseUrl": "https://www.packyapi.com/v1" },
|
|
201
|
+
"responses": { "api": "openai-responses", "baseUrl": "https://www.packyapi.com/v1" },
|
|
202
|
+
"messages": { "api": "anthropic-messages", "baseUrl": "https://www.packyapi.com" }
|
|
203
|
+
},
|
|
204
|
+
"defaultChannel": "chat",
|
|
205
|
+
"models": [
|
|
206
|
+
{ "id": "kimi-k2.5", "channels": ["chat", "messages"] },
|
|
207
|
+
{ "id": "grok-4.7", "channels": ["responses"] },
|
|
208
|
+
{ "id": "deepseek-v4-flash", "channels": ["chat", "responses", "messages"] },
|
|
209
|
+
{ "id": "glm-5" }
|
|
210
|
+
]
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
- **渠道字段**:`api`、`baseUrl` 必填;`apiKey`(同供应商级写法,`$ENV` / `!command` / 字面量;auth.json 里
|
|
217
|
+
`"<provider>@<channel>"` 条目同样生效)、`headers`、`compat`、`authHeader` 可选,缺省继承供应商级。渠道名
|
|
218
|
+
`[A-Za-z0-9][A-Za-z0-9_-]*`,不含 `/` 与 `@`。
|
|
219
|
+
- **模型挂载**:`models[].channels` 列出可用渠道,第一个是首选;不写 → `defaultChannel`(缺省为 `channels`
|
|
220
|
+
的第一个键)。引用了不存在的渠道 → 配置校验报带路径的错误。
|
|
221
|
+
- **模型引用**:`provider/model` 走首选渠道;`provider/model@channel` 显式指定(`--model`、`defaultModel`、
|
|
222
|
+
`/model`、SDK、RPC `set_model` 一致)。指定的渠道不在该模型的 `channels` 里 → 报错并列出可用的
|
|
223
|
+
`provider/model@channel`。`@` 之后不是该供应商的渠道名时整串仍按模型 id 处理(兼容 id 里本来带 `@` 的模型)。
|
|
224
|
+
- **向后兼容**:没有 `channels` 的供应商(含全部内置供应商)按单渠道处理——供应商级 `api` + `baseUrl`
|
|
225
|
+
就是隐式的 `default` 渠道;模型级 `api` / `baseUrl` 仍然有效,覆盖在所选渠道之上(等价于一个匿名渠道)。
|
|
226
|
+
已有配置不用改。写了 `channels` 时供应商级 `api` / `baseUrl` 不再单独成渠道。
|
|
227
|
+
- **运行时**:选中的模型带上该渠道的协议、地址、key、headers 与 compat;会话记录(`model_change`)与
|
|
228
|
+
`ModelRef` 带上 `channel`;缓存的端点键(三态、未命中、粒度推断)是 `供应商|主机|模型@渠道`,同一模型的
|
|
229
|
+
不同渠道分开统计。价格与 models.dev 元数据按模型共享。
|
|
230
|
+
|
|
231
|
+
### 一键接入:`ama providers`
|
|
232
|
+
|
|
233
|
+
只有 baseUrl 与 key 时,一条命令建好供应商、列出模型、补齐元数据:
|
|
234
|
+
|
|
235
|
+
```sh
|
|
236
|
+
ama providers add packy --base-url https://www.packyapi.com/v1 --key-env PACKY_API_KEY --probe --limit 8 --yes
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
```
|
|
240
|
+
ama providers add <id> --base-url <url> [--channel <name>=<api>@<baseUrl> …] [--api <api>|auto]
|
|
241
|
+
[--key-env <VAR>] [--probe] [--limit N] [--probe-models a,b,…]
|
|
242
|
+
[--max-requests N] [--prefer chat,responses,messages] [--include-no-tools] [--yes]
|
|
243
|
+
ama providers list
|
|
244
|
+
ama providers channels <id>
|
|
245
|
+
ama providers remove <id>
|
|
246
|
+
ama providers refresh <id> [--probe …]
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
- **key**:缺省从 stdin 读(终端下不回显,不进命令行与 shell 历史),存 `auth.json`(0600);给
|
|
250
|
+
`--key-env VAR` 时不读 stdin,`config.json` 里写 `"apiKey": "$VAR"`。供应商已有 key 时不再询问。
|
|
251
|
+
- **候选渠道**:给了 `--channel`(可重复)就只用这些;否则从 `--base-url` 推出三个——`chat`
|
|
252
|
+
(openai-completions,baseUrl 原样)、`responses`(openai-responses,同上)、`messages`
|
|
253
|
+
(anthropic-messages,去掉末尾 `/v1` 的主机根);`--api <api>` 只留对应的一个。
|
|
254
|
+
- **模型列表**:`GET {baseUrl}/models`(第一个 OpenAI 系渠道的地址;只有 Messages 渠道时是 `/v1/models`,先带
|
|
255
|
+
`Authorization: Bearer`,401 / 403 再试 `x-api-key`——实测中转只认前者)。new-api 一类中转在条目上给
|
|
256
|
+
`supported_endpoint_types`(`openai` / `openai-response` / `anthropic`),据此把模型挂到对应渠道;没有提示
|
|
257
|
+
时挂到全部候选渠道里的第一个;有提示但候选渠道都不支持(如只配了 messages 渠道时的 grok)→ 不写入。
|
|
258
|
+
- **`--probe`**:对选中的模型(`--probe-models` 列出的,缺省按 id 字母序不分大小写取前 `--limit` 个,缺省 30)
|
|
259
|
+
逐个渠道发一次最小请求(有提示时只试提示里的渠道),**探测成功的渠道全部写进模型的 `channels`**,顺序按
|
|
260
|
+
`--prefer`(缺省 chat、responses、messages);全部失败的模型不写入;未探测的按提示写入并在表格里标「未探测」。
|
|
261
|
+
执行前打印请求数预估,超过 `--max-requests`(缺省 60)时截断模型数;401 / 403 / 429 立即停止。
|
|
262
|
+
- **渠道收敛**:写入前删掉没有任何模型挂载的候选渠道;`defaultChannel` 取剩下的第一个(按 `--prefer`)。
|
|
263
|
+
- **写入**:用户级 `config.json` 的 `providers.<id>`(先备份为 `config.json.bak`)。模型条目只写 `id` 与
|
|
264
|
+
`channels`;上下文、输出、图像、推理、价格**不写进配置**,运行时从 models.dev 缓存补(见下节),所以
|
|
265
|
+
`refresh` 不会覆盖手改的字段,手写的值永远优先。models.dev 标明不支持工具调用的模型缺省不写入(Agent
|
|
266
|
+
离不开工具调用),`--include-no-tools` 照写。对已存在的供应商再执行 `add`:只追加新渠道与新模型,已有
|
|
267
|
+
渠道定义与模型条目一字不改。
|
|
268
|
+
- **确认**:写配置前打印摘要,终端里问一次 y/N;非 TTY 必须带 `--yes`(否则退出 2)。
|
|
269
|
+
- 打印表格:id、渠道、上下文、输出、图像、推理、工具调用、价格(models.dev 的原厂价,$/M 输入 / 输出,
|
|
270
|
+
中转实际价格可能不同)、匹配方式。
|
|
271
|
+
- `list`:全部供应商(config.json 里的与有 key 的内置供应商)→ 渠道(协议、地址、key 来源:auth.json /
|
|
272
|
+
`$VAR` / 字面量 / 无,从不显示 key)→ 模型数。`channels <id>`:每个渠道的协议、地址与挂载的模型数。
|
|
273
|
+
`remove`:删 `providers.<id>`(备份)与 auth.json 里该供应商的条目。`refresh`:重拉 `/models` 与
|
|
274
|
+
models.dev,只追加新模型(带 `--probe` 时同 add 的探测),已有条目不改;上游已下架的 id 只提示、不删。
|
|
275
|
+
|
|
276
|
+
实测(2026-10-02,一家同时提供三种接口的中转,22 个模型,共 29 次请求):
|
|
277
|
+
|
|
278
|
+
- `providers add --probe --probe-models kimi-k2.5,grok-4.7,deepseek-v4-flash` 共 8 次请求(1 次列表 + 7 次探测):
|
|
279
|
+
kimi-k2.5 → chat、messages(Responses 不通);grok-4.7 → responses;deepseek-v4-flash 在 Responses 上回
|
|
280
|
+
`incomplete_details.reason: "length"`(中转转发 DeepSeek 时不写 `max_output_tokens`),已按输出截断处理,三种接口都通。
|
|
281
|
+
- models.dev 匹配 22 / 22:原厂条目 19 个(其中 `qwen3.8-max-0902` 去日期后缀匹配到 `alibaba/qwen3.8-max`),多数一致 2 个
|
|
282
|
+
(kimi-k2.5:原厂 `moonshotai/kimi-k2.5` 不在库里,同 canonical 的 11 条取多数 262k / 图像;qwen3-coder-next),
|
|
283
|
+
唯一条目 1 个(qwen3-vl-flash)。
|
|
284
|
+
- `-p` 经 chat 与 `@messages` 两条渠道都正常;`--image` 四色方块图在 kimi-k2.5(chat、messages)与 qwen3-vl-flash 上都答对
|
|
285
|
+
红 / 绿 / 蓝 / 黄;对 glm-5(models.dev 标纯文本)直接退出 2、不发请求。
|
|
286
|
+
- 第二个供应商只配 messages 渠道:19 个模型挂上,3 个只支持 Responses 的 grok 不写入;同名模型不加前缀时报歧义并列出两家。
|
|
287
|
+
|
|
288
|
+
### 模型元数据:models.dev
|
|
289
|
+
|
|
290
|
+
[models.dev](https://models.dev) 汇总了两百多家供应商的模型参数(`https://models.dev/api.json`,约 5 MB)。
|
|
291
|
+
ama 用它给**没写元数据**的自定义模型补上下文、输出上限、输入模态、推理、价格与工具调用能力。
|
|
292
|
+
|
|
293
|
+
- **何时联网**:只有 `ama providers add|refresh`、`ama models discover`、`ama models refresh-catalog`
|
|
294
|
+
会拉取;**启动不联网**,只读缓存。缓存在数据目录 `models-dev.json`(缺省 `~/.local/share/ama/`,只留用到的
|
|
295
|
+
字段,约 2.3 MB),记获取时间与 ETag,24 小时内不重拉(`refresh-catalog` 强制,带 `If-None-Match`)。离线或
|
|
296
|
+
失败时用旧缓存并 warning。`AMA_MODELS_DEV_URL` 换数据源(镜像或本地文件服务)。
|
|
297
|
+
- **优先级**:用户配置(`models[]` / `modelOverrides[]` 里写了的字段)> 内置目录 > models.dev > 自定义缺省
|
|
298
|
+
(`maxTokens: 8192`、`input: ["text"]`、`reasoning: false`、不猜 `contextWindow`)。`ama models list` 与
|
|
299
|
+
`ama config show` 标出每个字段来自哪里(`config` / `目录` / `models.dev` / `缺省`)。
|
|
300
|
+
- **字段映射**:`contextWindow = limit.context`;`maxTokens = min(limit.output, 65536, contextWindow)`——
|
|
301
|
+
`maxTokens` 每次请求都作为 `max_tokens` 发出,models.dev 给的是原厂上限(不少模型写的是与上下文相同的
|
|
302
|
+
1M),中转换了上游后常拒收超大值,Anthropic 协议的思考预算也从它推导,64k 对编码 Agent 的单轮输出足够,
|
|
303
|
+
需要更大时在配置里写;`input` 由 `modalities.input` 含不含 `image` 定为 `["text","image"]` 或 `["text"]`;
|
|
304
|
+
`reasoning`;`cost` 取 `input` / `output` / `cache_read` / `cache_write`($/M),缺缓存价时按输入价算
|
|
305
|
+
(不假设有折扣,保温的经济性判断因此偏保守)。
|
|
306
|
+
- **匹配规则**(同一个 id 常在几十家转售商下重复出现,取值不一):
|
|
307
|
+
1. 模型上写了 `"modelsDev": "provider/model"` → 直接用该条目(写 `false` 关闭补全);
|
|
308
|
+
2. id 形如 `vendor/model` 且 models.dev 正好有这个 `provider/model` → 用它;
|
|
309
|
+
3. 按 id 不分大小写找全部同名条目;有 `canonical_model_id` 的,取指向与 id 同名的那个(否则取票数最多的),
|
|
310
|
+
它若能在原厂供应商下找到 → 用原厂条目;
|
|
311
|
+
4. 否则在(同一 canonical 的)条目里优先原厂供应商:anthropic、openai、google、deepseek、moonshotai(-cn)、
|
|
312
|
+
zhipuai、zai、alibaba(-cn)、xai、mistral、minimax(-cn)、llama(Meta)、cohere、xiaomi、stepfun 等
|
|
313
|
+
(models.dev 里没有 `qwen` / `meta` 这样的供应商 id,通义在 `alibaba`,Llama 在 `llama`);
|
|
314
|
+
5. 仍有多条 → 按 (上下文, 输出, 图像) 取多数,取值不一时记 warning;只有一条就用它;
|
|
315
|
+
6. 同名找不到时依次试归一化后的 id:去 `vendor/` 前缀、去 `:free` 一类后缀、去 `-latest`、去日期后缀
|
|
316
|
+
(`-0902`、`-20250514`、`-2025-05-14`);
|
|
317
|
+
7. 都没有 → 「未匹配」,保持自定义缺省(不猜 `contextWindow`,自动压缩关闭)。
|
|
318
|
+
|
|
319
|
+
### 图像输入
|
|
320
|
+
|
|
321
|
+
- 四条协议都把图片放进用户消息:Chat Completions `image_url`(data URL)、Responses `input_image`、
|
|
322
|
+
Anthropic `image`(base64 source)、Gemini `inlineData`;工具结果里的图片同样映射。
|
|
323
|
+
- 入口:`ama -p "描述这张图" --image a.png --image b.jpg`;交互界面与行式界面里写 `@图片路径`,或粘贴 /
|
|
324
|
+
拖入一个图片文件路径(整段输入里以 `.png` / `.jpg` / `.jpeg` / `.gif` / `.webp` 结尾且文件存在的词)。
|
|
325
|
+
- 与 `read` 工具共用 MIME 检测(按文件头识别 PNG / JPEG / GIF / WebP,扩展名不符时以文件头为准)与大小上限
|
|
326
|
+
(单张 5 MB,取各家上限中最小的 Anthropic)。
|
|
327
|
+
- 模型 `input` 不含 `image` 时直接拒绝并提示换模型(`-p` 退出 2,界面里给错误提示,不发请求);`read`
|
|
328
|
+
工具读图时只返回路径、尺寸与「当前模型不接受图片」。
|
|
329
|
+
|
|
115
330
|
接好之后:`ama models check packy/<id>` 发一次最小请求确认连通;`ama models cache-probe packy/<id>` 看这个端点报不报缓存(见下节「缓存」),中转上不报缓存的模型按建议设 `compat.cacheReporting: "silent"`,状态栏就显示「未报告」而不是 0%。
|
|
116
331
|
|
|
117
332
|
## OpenAI 兼容线的 compat
|