claude-glm-alt-installer 2.0.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.
@@ -0,0 +1,153 @@
1
+ // Main Fastify server that routes requests by provider prefix
2
+ import Fastify from "fastify";
3
+ import { parseProviderModel, warnIfTools } from "./map.js";
4
+ import type { AnthropicRequest, ProviderModel } from "./types.js";
5
+ import { chatOpenAI } from "./providers/openai.js";
6
+ import { chatOpenRouter } from "./providers/openrouter.js";
7
+ import { chatGemini } from "./providers/gemini.js";
8
+ import { passThrough } from "./providers/anthropic-pass.js";
9
+ import { config } from "dotenv";
10
+ import { join } from "path";
11
+ import { homedir } from "os";
12
+
13
+ // Load .env from ~/.claude-proxy/.env
14
+ const envPath = join(homedir(), ".claude-proxy", ".env");
15
+ config({ path: envPath });
16
+
17
+ const PORT = Number(process.env.CLAUDE_PROXY_PORT || 17870);
18
+
19
+ let active: ProviderModel | null = null;
20
+
21
+ const fastify = Fastify({ logger: false });
22
+
23
+ // Health check endpoint
24
+ fastify.get("/healthz", async () => ({
25
+ ok: true,
26
+ active: active ?? { provider: "glm", model: "auto" }
27
+ }));
28
+
29
+ // Status endpoint (shows current active provider/model)
30
+ fastify.get("/_status", async () => {
31
+ return active ?? { provider: "glm", model: "glm-4.7" };
32
+ });
33
+
34
+ // Main messages endpoint - routes by model prefix
35
+ fastify.post("/v1/messages", async (req, res) => {
36
+ try {
37
+ const body = req.body as AnthropicRequest;
38
+ const defaults = active ?? undefined;
39
+ const { provider, model } = parseProviderModel(body.model, defaults);
40
+
41
+ // Warn if using tools with providers that may not support them
42
+ warnIfTools(body, provider);
43
+
44
+ active = { provider, model };
45
+
46
+ // Validate API keys BEFORE setting headers
47
+ if (provider === "openai") {
48
+ const key = process.env.OPENAI_API_KEY;
49
+ if (!key) {
50
+ throw apiError(401, "OPENAI_API_KEY not set in ~/.claude-proxy/.env");
51
+ }
52
+ // Set headers only after validation
53
+ res.raw.setHeader("Content-Type", "text/event-stream");
54
+ res.raw.setHeader("Cache-Control", "no-cache, no-transform");
55
+ res.raw.setHeader("Connection", "keep-alive");
56
+ // @ts-ignore
57
+ res.raw.flushHeaders?.();
58
+ return chatOpenAI(res, body, model, key);
59
+ }
60
+
61
+ if (provider === "openrouter") {
62
+ const key = process.env.OPENROUTER_API_KEY;
63
+ if (!key) {
64
+ throw apiError(401, "OPENROUTER_API_KEY not set in ~/.claude-proxy/.env");
65
+ }
66
+ res.raw.setHeader("Content-Type", "text/event-stream");
67
+ res.raw.setHeader("Cache-Control", "no-cache, no-transform");
68
+ res.raw.setHeader("Connection", "keep-alive");
69
+ // @ts-ignore
70
+ res.raw.flushHeaders?.();
71
+ return chatOpenRouter(res, body, model, key);
72
+ }
73
+
74
+ if (provider === "gemini") {
75
+ const key = process.env.GEMINI_API_KEY;
76
+ if (!key) {
77
+ throw apiError(401, "GEMINI_API_KEY not set in ~/.claude-proxy/.env");
78
+ }
79
+ res.raw.setHeader("Content-Type", "text/event-stream");
80
+ res.raw.setHeader("Cache-Control", "no-cache, no-transform");
81
+ res.raw.setHeader("Connection", "keep-alive");
82
+ // @ts-ignore
83
+ res.raw.flushHeaders?.();
84
+ return chatGemini(res, body, model, key);
85
+ }
86
+
87
+ if (provider === "anthropic") {
88
+ const base = process.env.ANTHROPIC_UPSTREAM_URL;
89
+ const key = process.env.ANTHROPIC_API_KEY;
90
+ if (!base || !key) {
91
+ throw apiError(
92
+ 500,
93
+ "ANTHROPIC_UPSTREAM_URL and ANTHROPIC_API_KEY not set in ~/.claude-proxy/.env"
94
+ );
95
+ }
96
+ // Don't set headers here - passThrough will do it after validation
97
+ return passThrough({
98
+ res,
99
+ body,
100
+ model,
101
+ baseUrl: base,
102
+ headers: {
103
+ "Content-Type": "application/json",
104
+ "x-api-key": key,
105
+ "anthropic-version": process.env.ANTHROPIC_VERSION || "2023-06-01"
106
+ }
107
+ });
108
+ }
109
+
110
+ // Default: glm (Z.AI)
111
+ const glmBase = process.env.GLM_UPSTREAM_URL;
112
+ const glmKey = process.env.ZAI_API_KEY || process.env.GLM_API_KEY;
113
+ if (!glmBase || !glmKey) {
114
+ throw apiError(
115
+ 500,
116
+ "GLM_UPSTREAM_URL and ZAI_API_KEY not set in ~/.claude-proxy/.env. Run: ccx --setup"
117
+ );
118
+ }
119
+ // Don't set headers here - passThrough will do it after validation
120
+ return passThrough({
121
+ res,
122
+ body,
123
+ model,
124
+ baseUrl: glmBase,
125
+ headers: {
126
+ "Content-Type": "application/json",
127
+ Authorization: `Bearer ${glmKey}`,
128
+ "anthropic-version": process.env.ANTHROPIC_VERSION || "2023-06-01"
129
+ }
130
+ });
131
+ } catch (e: any) {
132
+ const status = e?.statusCode ?? 500;
133
+ return res.code(status).send({ error: e?.message || "proxy error" });
134
+ }
135
+ });
136
+
137
+ function apiError(status: number, message: string) {
138
+ const e = new Error(message);
139
+ // @ts-ignore
140
+ e.statusCode = status;
141
+ return e;
142
+ }
143
+
144
+ fastify
145
+ .listen({ port: PORT, host: "127.0.0.1" })
146
+ .then(() => {
147
+ console.log(`[ccx] Proxy listening on http://127.0.0.1:${PORT}`);
148
+ console.log(`[ccx] Configure API keys in: ${envPath}`);
149
+ })
150
+ .catch((err) => {
151
+ console.error("[ccx] Failed to start proxy:", err.message);
152
+ process.exit(1);
153
+ });
@@ -0,0 +1,83 @@
1
+ // Provider parsing and message mapping utilities
2
+ import { AnthropicMessage, AnthropicRequest, ProviderKey, ProviderModel } from "./types.js";
3
+
4
+ const PROVIDER_PREFIXES: ProviderKey[] = ["openai", "openrouter", "gemini", "glm", "anthropic"];
5
+
6
+ /**
7
+ * Parse provider and model from the model field
8
+ * Supports formats: "provider:model" or "provider/model"
9
+ * Falls back to defaults if no valid prefix found
10
+ */
11
+ export function parseProviderModel(modelField: string, defaults?: ProviderModel): ProviderModel {
12
+ if (!modelField) {
13
+ if (defaults) return defaults;
14
+ throw new Error("Missing 'model' in request");
15
+ }
16
+
17
+ const sep = modelField.includes(":") ? ":" : modelField.includes("/") ? "/" : null;
18
+ if (!sep) {
19
+ // no prefix: fall back to defaults or assume glm as legacy
20
+ return defaults ?? { provider: "glm", model: modelField };
21
+ }
22
+
23
+ const [maybeProv, ...rest] = modelField.split(sep);
24
+ const prov = maybeProv.toLowerCase() as ProviderKey;
25
+
26
+ if (!PROVIDER_PREFIXES.includes(prov)) {
27
+ // unrecognized prefix -> use defaults or treat full string as model
28
+ return defaults ?? { provider: "glm", model: modelField };
29
+ }
30
+
31
+ return { provider: prov, model: rest.join(sep) };
32
+ }
33
+
34
+ /**
35
+ * Warn if tools are being used with providers that may not support them
36
+ */
37
+ export function warnIfTools(req: AnthropicRequest, provider: ProviderKey): void {
38
+ if (req.tools && req.tools.length > 0) {
39
+ // Only GLM and Anthropic support tools natively
40
+ if (provider !== "glm" && provider !== "anthropic") {
41
+ console.warn(`[proxy] Warning: ${provider} may not fully support Anthropic-style tools. Passing through anyway.`);
42
+ }
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Convert Anthropic content to plain text
48
+ */
49
+ export function toPlainText(content: AnthropicMessage["content"]): string {
50
+ if (typeof content === "string") return content;
51
+ return content
52
+ .map((c) => {
53
+ if (typeof c === "string") return c;
54
+ if (c.type === "text") return c.text;
55
+ if (c.type === "tool_result") {
56
+ // Convert tool results to text representation
57
+ if (typeof c.content === "string") return c.content;
58
+ return JSON.stringify(c.content);
59
+ }
60
+ return "";
61
+ })
62
+ .join("");
63
+ }
64
+
65
+ /**
66
+ * Convert Anthropic messages to OpenAI format
67
+ */
68
+ export function toOpenAIMessages(messages: AnthropicMessage[]) {
69
+ return messages.map((m) => ({
70
+ role: m.role,
71
+ content: toPlainText(m.content)
72
+ }));
73
+ }
74
+
75
+ /**
76
+ * Convert Anthropic messages to Gemini format
77
+ */
78
+ export function toGeminiContents(messages: AnthropicMessage[]) {
79
+ return messages.map((m) => ({
80
+ role: m.role === "assistant" ? "model" : "user",
81
+ parts: [{ text: toPlainText(m.content) }]
82
+ }));
83
+ }
@@ -0,0 +1,64 @@
1
+ // Pass-through adapter for Anthropic-compatible upstreams (Anthropic API and Z.AI GLM)
2
+ import { FastifyReply } from "fastify";
3
+
4
+ type PassArgs = {
5
+ res: FastifyReply;
6
+ body: any;
7
+ model: string;
8
+ baseUrl: string;
9
+ headers: Record<string, string>;
10
+ };
11
+
12
+ /**
13
+ * Pass through requests to Anthropic-compatible APIs
14
+ * This works for both:
15
+ * - Anthropic's official API
16
+ * - Z.AI's GLM API (Anthropic-compatible)
17
+ */
18
+ export async function passThrough({ res, body, model, baseUrl, headers }: PassArgs) {
19
+ const url = `${stripEndSlash(baseUrl)}/v1/messages`;
20
+
21
+ // Ensure stream is true for Claude Code UX
22
+ body.stream = true;
23
+
24
+ const resp = await fetch(url, {
25
+ method: "POST",
26
+ headers,
27
+ body: JSON.stringify(body)
28
+ });
29
+
30
+ if (!resp.ok || !resp.body) {
31
+ const text = await safeText(resp);
32
+ const err = new Error(`Upstream error (${resp.status}): ${text}`);
33
+ // @ts-ignore
34
+ err.statusCode = resp.status || 502;
35
+ throw err;
36
+ }
37
+
38
+ // Pipe upstream SSE as-is (already in Anthropic format)
39
+ res.raw.setHeader("Content-Type", "text/event-stream");
40
+ res.raw.setHeader("Cache-Control", "no-cache, no-transform");
41
+ res.raw.setHeader("Connection", "keep-alive");
42
+ // @ts-ignore
43
+ res.raw.flushHeaders?.();
44
+
45
+ const reader = resp.body.getReader();
46
+ while (true) {
47
+ const { value, done } = await reader.read();
48
+ if (done) break;
49
+ res.raw.write(value);
50
+ }
51
+ res.raw.end();
52
+ }
53
+
54
+ function stripEndSlash(s: string) {
55
+ return s.endsWith("/") ? s.slice(0, -1) : s;
56
+ }
57
+
58
+ async function safeText(resp: Response) {
59
+ try {
60
+ return await resp.text();
61
+ } catch {
62
+ return "<no-body>";
63
+ }
64
+ }
@@ -0,0 +1,89 @@
1
+ // Gemini adapter using streamGenerateContent (SSE)
2
+ import { FastifyReply } from "fastify";
3
+ import { createParser } from "eventsource-parser";
4
+ import { deltaText, startAnthropicMessage, stopAnthropicMessage } from "../sse.js";
5
+ import { toGeminiContents } from "../map.js";
6
+ import type { AnthropicRequest } from "../types.js";
7
+
8
+ const G_BASE = process.env.GEMINI_BASE_URL || "https://generativelanguage.googleapis.com/v1beta";
9
+
10
+ export async function chatGemini(
11
+ res: FastifyReply,
12
+ body: AnthropicRequest,
13
+ model: string,
14
+ apiKey?: string
15
+ ) {
16
+ if (!apiKey) {
17
+ throw withStatus(401, "Missing GEMINI_API_KEY. Set it in ~/.claude-proxy/.env");
18
+ }
19
+
20
+ const url = `${G_BASE}/models/${encodeURIComponent(model)}:streamGenerateContent?alt=sse&key=${apiKey}`;
21
+
22
+ const reqBody: any = {
23
+ contents: toGeminiContents(body.messages),
24
+ generationConfig: {
25
+ temperature: body.temperature ?? 0.7,
26
+ maxOutputTokens: body.max_tokens
27
+ }
28
+ };
29
+
30
+ // Note: Gemini has different tool format, just warn for now
31
+ if (body.tools && body.tools.length > 0) {
32
+ console.warn("[gemini] Tools not yet adapted to Gemini format, skipping");
33
+ }
34
+
35
+ const resp = await fetch(url, {
36
+ method: "POST",
37
+ headers: { "Content-Type": "application/json" },
38
+ body: JSON.stringify(reqBody)
39
+ });
40
+
41
+ if (!resp.ok || !resp.body) {
42
+ const text = await safeText(resp);
43
+ throw withStatus(resp.status || 500, `Gemini error: ${text}`);
44
+ }
45
+
46
+ startAnthropicMessage(res, model);
47
+
48
+ const reader = resp.body.getReader();
49
+ const decoder = new TextDecoder();
50
+ const parser = createParser((event) => {
51
+ if (event.type !== "event") return;
52
+ const data = event.data;
53
+ if (!data) return;
54
+ try {
55
+ const json = JSON.parse(data);
56
+ // Gemini response: candidates[0].content.parts[].text
57
+ const text =
58
+ json?.candidates?.[0]?.content?.parts
59
+ ?.map((p: any) => p?.text || "")
60
+ .join("") || "";
61
+ if (text) deltaText(res, text);
62
+ } catch {
63
+ // ignore parse errors
64
+ }
65
+ });
66
+
67
+ while (true) {
68
+ const { value, done } = await reader.read();
69
+ if (done) break;
70
+ parser.feed(decoder.decode(value));
71
+ }
72
+
73
+ stopAnthropicMessage(res);
74
+ }
75
+
76
+ function withStatus(status: number, message: string) {
77
+ const e = new Error(message);
78
+ // @ts-ignore
79
+ e.statusCode = status;
80
+ return e;
81
+ }
82
+
83
+ async function safeText(resp: Response) {
84
+ try {
85
+ return await resp.text();
86
+ } catch {
87
+ return "<no-body>";
88
+ }
89
+ }
@@ -0,0 +1,90 @@
1
+ // OpenAI adapter using chat.completions with SSE streaming
2
+ import { FastifyReply } from "fastify";
3
+ import { createParser } from "eventsource-parser";
4
+ import { deltaText, startAnthropicMessage, stopAnthropicMessage } from "../sse.js";
5
+ import { toOpenAIMessages } from "../map.js";
6
+ import type { AnthropicRequest } from "../types.js";
7
+
8
+ const OPENAI_BASE = process.env.OPENAI_BASE_URL || "https://api.openai.com/v1";
9
+
10
+ export async function chatOpenAI(
11
+ res: FastifyReply,
12
+ body: AnthropicRequest,
13
+ model: string,
14
+ apiKey?: string
15
+ ) {
16
+ if (!apiKey) {
17
+ throw withStatus(401, "Missing OPENAI_API_KEY. Set it in ~/.claude-proxy/.env");
18
+ }
19
+
20
+ const url = `${OPENAI_BASE}/chat/completions`;
21
+
22
+ const oaiBody: any = {
23
+ model,
24
+ messages: toOpenAIMessages(body.messages),
25
+ stream: true,
26
+ temperature: body.temperature ?? 0.7,
27
+ max_tokens: body.max_tokens
28
+ };
29
+
30
+ // Pass through tools if provided (note: OpenAI format may differ)
31
+ if (body.tools && body.tools.length > 0) {
32
+ console.warn("[openai] Tools passed through but format may not be compatible");
33
+ oaiBody.tools = body.tools;
34
+ }
35
+
36
+ const resp = await fetch(url, {
37
+ method: "POST",
38
+ headers: {
39
+ Authorization: `Bearer ${apiKey}`,
40
+ "Content-Type": "application/json"
41
+ },
42
+ body: JSON.stringify(oaiBody)
43
+ });
44
+
45
+ if (!resp.ok || !resp.body) {
46
+ const text = await safeText(resp);
47
+ throw withStatus(resp.status || 500, `OpenAI error: ${text}`);
48
+ }
49
+
50
+ // Emit Anthropic SSE start events
51
+ startAnthropicMessage(res, model);
52
+
53
+ const reader = resp.body.getReader();
54
+ const decoder = new TextDecoder();
55
+ const parser = createParser((event) => {
56
+ if (event.type !== "event") return;
57
+ const data = event.data;
58
+ if (!data || data === "[DONE]") return;
59
+ try {
60
+ const json = JSON.parse(data);
61
+ const chunk = json.choices?.[0]?.delta?.content ?? "";
62
+ if (chunk) deltaText(res, chunk);
63
+ } catch {
64
+ // ignore parse errors on keepalives, etc.
65
+ }
66
+ });
67
+
68
+ while (true) {
69
+ const { value, done } = await reader.read();
70
+ if (done) break;
71
+ parser.feed(decoder.decode(value));
72
+ }
73
+
74
+ stopAnthropicMessage(res);
75
+ }
76
+
77
+ function withStatus(status: number, message: string) {
78
+ const e = new Error(message);
79
+ // @ts-ignore
80
+ e.statusCode = status;
81
+ return e;
82
+ }
83
+
84
+ async function safeText(resp: Response) {
85
+ try {
86
+ return await resp.text();
87
+ } catch {
88
+ return "<no-body>";
89
+ }
90
+ }
@@ -0,0 +1,98 @@
1
+ // OpenRouter adapter (OpenAI-compatible API)
2
+ import { FastifyReply } from "fastify";
3
+ import { createParser } from "eventsource-parser";
4
+ import { deltaText, startAnthropicMessage, stopAnthropicMessage } from "../sse.js";
5
+ import { toOpenAIMessages } from "../map.js";
6
+ import type { AnthropicRequest } from "../types.js";
7
+
8
+ const OR_BASE = process.env.OPENROUTER_BASE_URL || "https://openrouter.ai/api/v1";
9
+
10
+ export async function chatOpenRouter(
11
+ res: FastifyReply,
12
+ body: AnthropicRequest,
13
+ model: string,
14
+ apiKey?: string
15
+ ) {
16
+ if (!apiKey) {
17
+ throw withStatus(401, "Missing OPENROUTER_API_KEY. Set it in ~/.claude-proxy/.env");
18
+ }
19
+
20
+ const url = `${OR_BASE}/chat/completions`;
21
+ const headers: Record<string, string> = {
22
+ Authorization: `Bearer ${apiKey}`,
23
+ "Content-Type": "application/json"
24
+ };
25
+
26
+ // Add optional OpenRouter headers
27
+ if (process.env.OPENROUTER_REFERER) {
28
+ headers["HTTP-Referer"] = process.env.OPENROUTER_REFERER;
29
+ }
30
+ if (process.env.OPENROUTER_TITLE) {
31
+ headers["X-Title"] = process.env.OPENROUTER_TITLE;
32
+ }
33
+
34
+ const reqBody: any = {
35
+ model,
36
+ messages: toOpenAIMessages(body.messages),
37
+ stream: true,
38
+ temperature: body.temperature ?? 0.7,
39
+ max_tokens: body.max_tokens
40
+ };
41
+
42
+ // Pass through tools if provided
43
+ if (body.tools && body.tools.length > 0) {
44
+ console.warn("[openrouter] Tools passed through but format may not be compatible");
45
+ reqBody.tools = body.tools;
46
+ }
47
+
48
+ const resp = await fetch(url, {
49
+ method: "POST",
50
+ headers,
51
+ body: JSON.stringify(reqBody)
52
+ });
53
+
54
+ if (!resp.ok || !resp.body) {
55
+ const text = await safeText(resp);
56
+ throw withStatus(resp.status || 500, `OpenRouter error: ${text}`);
57
+ }
58
+
59
+ startAnthropicMessage(res, model);
60
+
61
+ const reader = resp.body.getReader();
62
+ const decoder = new TextDecoder();
63
+ const parser = createParser((event) => {
64
+ if (event.type !== "event") return;
65
+ const data = event.data;
66
+ if (!data || data === "[DONE]") return;
67
+ try {
68
+ const json = JSON.parse(data);
69
+ const chunk = json.choices?.[0]?.delta?.content ?? "";
70
+ if (chunk) deltaText(res, chunk);
71
+ } catch {
72
+ // ignore parse errors
73
+ }
74
+ });
75
+
76
+ while (true) {
77
+ const { value, done } = await reader.read();
78
+ if (done) break;
79
+ parser.feed(decoder.decode(value));
80
+ }
81
+
82
+ stopAnthropicMessage(res);
83
+ }
84
+
85
+ function withStatus(status: number, message: string) {
86
+ const e = new Error(message);
87
+ // @ts-ignore
88
+ e.statusCode = status;
89
+ return e;
90
+ }
91
+
92
+ async function safeText(resp: Response) {
93
+ try {
94
+ return await resp.text();
95
+ } catch {
96
+ return "<no-body>";
97
+ }
98
+ }
@@ -0,0 +1,62 @@
1
+ // Server-Sent Events (SSE) utilities for Anthropic-style streaming
2
+ import type { FastifyReply } from "fastify";
3
+
4
+ export function initSSE(res: FastifyReply) {
5
+ res.raw.setHeader("Content-Type", "text/event-stream");
6
+ res.raw.setHeader("Cache-Control", "no-cache, no-transform");
7
+ res.raw.setHeader("Connection", "keep-alive");
8
+ // @ts-ignore
9
+ res.raw.flushHeaders?.();
10
+ }
11
+
12
+ export function sendEvent(res: FastifyReply, event: string, data: unknown) {
13
+ res.raw.write(`event: ${event}\n`);
14
+ res.raw.write(`data: ${JSON.stringify(data)}\n\n`);
15
+ }
16
+
17
+ export function endSSE(res: FastifyReply) {
18
+ res.raw.write("event: done\n");
19
+ res.raw.write("data: {}\n\n");
20
+ res.raw.end();
21
+ }
22
+
23
+ export function startAnthropicMessage(res: FastifyReply, model: string) {
24
+ const id = `msg_${Date.now()}`;
25
+ sendEvent(res, "message_start", {
26
+ type: "message_start",
27
+ message: {
28
+ id,
29
+ type: "message",
30
+ role: "assistant",
31
+ model,
32
+ content: [],
33
+ stop_reason: null,
34
+ stop_sequence: null,
35
+ usage: { input_tokens: 0, output_tokens: 0 }
36
+ }
37
+ });
38
+ sendEvent(res, "content_block_start", {
39
+ type: "content_block_start",
40
+ index: 0,
41
+ content_block: { type: "text", text: "" }
42
+ });
43
+ }
44
+
45
+ export function deltaText(res: FastifyReply, text: string) {
46
+ if (!text) return;
47
+ sendEvent(res, "content_block_delta", {
48
+ type: "content_block_delta",
49
+ index: 0,
50
+ delta: { type: "text_delta", text }
51
+ });
52
+ }
53
+
54
+ export function stopAnthropicMessage(res: FastifyReply) {
55
+ sendEvent(res, "content_block_stop", { type: "content_block_stop", index: 0 });
56
+ sendEvent(res, "message_delta", {
57
+ type: "message_delta",
58
+ delta: { stop_reason: "end_turn", stop_sequence: null },
59
+ usage: { output_tokens: 0 }
60
+ });
61
+ sendEvent(res, "message_stop", { type: "message_stop" });
62
+ }
@@ -0,0 +1,36 @@
1
+ // TypeScript type definitions for Anthropic API subset
2
+ // Used across all adapter files
3
+
4
+ export type AnthropicContentBlock =
5
+ | { type: "text"; text: string }
6
+ | { type: "image"; source: { type: "base64"; media_type: string; data: string } }
7
+ | { type: "tool_use"; id: string; name: string; input: unknown }
8
+ | { type: "tool_result"; tool_use_id: string; content: string | unknown[] };
9
+
10
+ export type AnthropicMessage = {
11
+ role: "user" | "assistant";
12
+ content: string | AnthropicContentBlock[];
13
+ };
14
+
15
+ export type AnthropicTool = {
16
+ name: string;
17
+ description?: string;
18
+ input_schema?: unknown;
19
+ };
20
+
21
+ export type AnthropicRequest = {
22
+ model: string;
23
+ messages: AnthropicMessage[];
24
+ max_tokens?: number;
25
+ temperature?: number;
26
+ stream?: boolean;
27
+ tools?: AnthropicTool[];
28
+ system?: string;
29
+ };
30
+
31
+ export type ProviderKey = "openai" | "openrouter" | "gemini" | "glm" | "anthropic";
32
+
33
+ export type ProviderModel = {
34
+ provider: ProviderKey;
35
+ model: string;
36
+ };