chatccc 0.2.245 → 0.2.247
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 +10 -8
- package/config.sample.json +2 -1
- package/deepccc-agent/README.md +20 -6
- package/deepccc-agent/package-lock.json +2027 -1959
- package/deepccc-agent/package.json +4 -2
- package/deepccc-agent/src/__tests__/chat-session.test.ts +59 -0
- package/deepccc-agent/src/__tests__/config.test.ts +9 -1
- package/deepccc-agent/src/__tests__/permissions.test.ts +4 -0
- package/deepccc-agent/src/__tests__/privacy.test.ts +7 -3
- package/deepccc-agent/src/cli.ts +18 -7
- package/deepccc-agent/src/config.ts +13 -0
- package/deepccc-agent/src/index.ts +47 -22
- package/package.json +2 -1
- package/src/__tests__/builtin-chat-session.test.ts +11 -1
- package/src/__tests__/builtin-permissions.test.ts +11 -3
- package/src/__tests__/ccc-adapter.test.ts +9 -0
- package/src/__tests__/config-reload.test.ts +1 -1
- package/src/__tests__/config-utils.test.ts +40 -0
- package/src/__tests__/session-ccc-config.test.ts +21 -0
- package/src/__tests__/web-ui.test.ts +2 -0
- package/src/adapters/ccc-adapter.ts +1 -0
- package/src/config-utils.ts +37 -13
- package/src/config.ts +23 -13
- package/src/session.ts +325 -323
- package/src/web-ui.ts +33 -2
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "deepccc",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "A lightweight
|
|
3
|
+
"version": "0.1.16",
|
|
4
|
+
"description": "A lightweight coding agent with OpenAI-compatible and Anthropic Messages API support.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"agent",
|
|
8
8
|
"deepseek",
|
|
9
9
|
"coding-agent",
|
|
10
10
|
"openai-compatible",
|
|
11
|
+
"anthropic",
|
|
11
12
|
"cli"
|
|
12
13
|
],
|
|
13
14
|
"repository": {
|
|
@@ -47,6 +48,7 @@
|
|
|
47
48
|
"typecheck": "node node_modules/typescript/bin/tsc --noEmit"
|
|
48
49
|
},
|
|
49
50
|
"dependencies": {
|
|
51
|
+
"@ai-sdk/anthropic": "^3.0.105",
|
|
50
52
|
"@ai-sdk/openai-compatible": "^2.0.47",
|
|
51
53
|
"@vscode/ripgrep": "^1.18.0",
|
|
52
54
|
"ai": "^6.0.184"
|
|
@@ -15,12 +15,18 @@ const rawLogWriteLineMock = vi.fn();
|
|
|
15
15
|
const rawLogCloseMock = vi.fn();
|
|
16
16
|
const originalRawStreamLogs = structuredClone(config.rawStreamLogs);
|
|
17
17
|
const originalStreaming = config.streaming;
|
|
18
|
+
const originalProvider = config.provider;
|
|
18
19
|
const createOpenAICompatibleMock = vi.fn(() => (modelId: string) => ({ modelId }));
|
|
20
|
+
const createAnthropicMock = vi.fn(() => (modelId: string) => ({ modelId, provider: "anthropic" }));
|
|
19
21
|
|
|
20
22
|
vi.mock("@ai-sdk/openai-compatible", () => ({
|
|
21
23
|
createOpenAICompatible: createOpenAICompatibleMock,
|
|
22
24
|
}));
|
|
23
25
|
|
|
26
|
+
vi.mock("@ai-sdk/anthropic", () => ({
|
|
27
|
+
createAnthropic: createAnthropicMock,
|
|
28
|
+
}));
|
|
29
|
+
|
|
24
30
|
vi.mock("ai", () => ({
|
|
25
31
|
streamText: streamTextMock,
|
|
26
32
|
generateText: generateTextMock,
|
|
@@ -49,6 +55,7 @@ async function* fullStream(...parts: unknown[]): AsyncIterable<unknown> {
|
|
|
49
55
|
}
|
|
50
56
|
|
|
51
57
|
beforeEach(() => {
|
|
58
|
+
config.provider = "openai";
|
|
52
59
|
config.streaming = true;
|
|
53
60
|
});
|
|
54
61
|
|
|
@@ -59,12 +66,64 @@ afterEach(() => {
|
|
|
59
66
|
rawLogWriteLineMock.mockReset();
|
|
60
67
|
rawLogCloseMock.mockReset();
|
|
61
68
|
config.rawStreamLogs = structuredClone(originalRawStreamLogs);
|
|
69
|
+
config.provider = originalProvider;
|
|
62
70
|
config.streaming = originalStreaming;
|
|
63
71
|
createOpenAICompatibleMock.mockClear();
|
|
72
|
+
createAnthropicMock.mockClear();
|
|
64
73
|
vi.useRealTimers();
|
|
65
74
|
});
|
|
66
75
|
|
|
67
76
|
describe("ChatSession response transport", () => {
|
|
77
|
+
it("uses the OpenAI-compatible provider by default", async () => {
|
|
78
|
+
const { ChatSession } = await import("../index.js");
|
|
79
|
+
|
|
80
|
+
new ChatSession({ apiKey: "sk-test", baseURL: "https://gateway.example", model: "model-a" });
|
|
81
|
+
|
|
82
|
+
expect(createOpenAICompatibleMock).toHaveBeenLastCalledWith(expect.objectContaining({
|
|
83
|
+
baseURL: "https://gateway.example",
|
|
84
|
+
apiKey: "sk-test",
|
|
85
|
+
}));
|
|
86
|
+
expect(createAnthropicMock).not.toHaveBeenCalled();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("uses Anthropic Messages with the same base URL and appends /v1 when needed", async () => {
|
|
90
|
+
const { ChatSession } = await import("../index.js");
|
|
91
|
+
|
|
92
|
+
new ChatSession({
|
|
93
|
+
provider: "anthropic",
|
|
94
|
+
apiKey: "sk-test",
|
|
95
|
+
baseURL: "https://gateway.example/",
|
|
96
|
+
model: "model-a",
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
expect(createAnthropicMock).toHaveBeenLastCalledWith({
|
|
100
|
+
baseURL: "https://gateway.example/v1",
|
|
101
|
+
apiKey: "sk-test",
|
|
102
|
+
});
|
|
103
|
+
expect(createOpenAICompatibleMock).not.toHaveBeenCalled();
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("keeps an existing /v1 suffix for Anthropic and streams without OpenAI-only effort options", async () => {
|
|
107
|
+
const { ChatSession } = await import("../index.js");
|
|
108
|
+
streamTextMock.mockReturnValueOnce({ textStream: textStream("done") });
|
|
109
|
+
const session = new ChatSession({
|
|
110
|
+
provider: "anthropic",
|
|
111
|
+
apiKey: "sk-test",
|
|
112
|
+
baseURL: "https://gateway.example/v1/",
|
|
113
|
+
model: "model-a",
|
|
114
|
+
effort: "high",
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
await collect(session.chat("hello"));
|
|
118
|
+
|
|
119
|
+
expect(createAnthropicMock).toHaveBeenLastCalledWith({
|
|
120
|
+
baseURL: "https://gateway.example/v1",
|
|
121
|
+
apiKey: "sk-test",
|
|
122
|
+
});
|
|
123
|
+
expect(streamTextMock).toHaveBeenCalledOnce();
|
|
124
|
+
expect(streamTextMock.mock.calls[0]?.[0]).not.toHaveProperty("providerOptions");
|
|
125
|
+
});
|
|
126
|
+
|
|
68
127
|
it("asks the provider to include usage in streaming responses", async () => {
|
|
69
128
|
const { ChatSession } = await import("../index.js");
|
|
70
129
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { afterEach, describe, expect, it } from "vitest";
|
|
2
2
|
|
|
3
3
|
import { ChatSession } from "../index.js";
|
|
4
|
-
import { config } from "../config.js";
|
|
4
|
+
import { config, normalizeDeepCccProvider } from "../config.js";
|
|
5
5
|
|
|
6
6
|
const originalDeepSeekApiKey = process.env.DEEPSEEK_API_KEY;
|
|
7
7
|
const originalDeepCccApiKey = config.apiKey;
|
|
@@ -16,6 +16,14 @@ afterEach(() => {
|
|
|
16
16
|
});
|
|
17
17
|
|
|
18
18
|
describe("builtin ChatSession config", () => {
|
|
19
|
+
it("defaults provider selection to openai and accepts anthropic case-insensitively", () => {
|
|
20
|
+
expect(normalizeDeepCccProvider(undefined)).toBe("openai");
|
|
21
|
+
expect(normalizeDeepCccProvider("")).toBe("openai");
|
|
22
|
+
expect(normalizeDeepCccProvider("OPENAI")).toBe("openai");
|
|
23
|
+
expect(normalizeDeepCccProvider("Anthropic")).toBe("anthropic");
|
|
24
|
+
expect(() => normalizeDeepCccProvider("antropic")).toThrow(/openai.*anthropic/i);
|
|
25
|
+
});
|
|
26
|
+
|
|
19
27
|
it("uses the builtin ~/.deepccc config when no apiKey is passed", () => {
|
|
20
28
|
expect(() => new ChatSession()).not.toThrow();
|
|
21
29
|
});
|
|
@@ -22,6 +22,10 @@ vi.mock("@ai-sdk/openai-compatible", () => ({
|
|
|
22
22
|
createOpenAICompatible: vi.fn(() => (modelId: string) => ({ modelId })),
|
|
23
23
|
}));
|
|
24
24
|
|
|
25
|
+
vi.mock("@ai-sdk/anthropic", () => ({
|
|
26
|
+
createAnthropic: vi.fn(() => (modelId: string) => ({ modelId })),
|
|
27
|
+
}));
|
|
28
|
+
|
|
25
29
|
vi.mock("ai", () => ({
|
|
26
30
|
streamText: aiMocks.streamText,
|
|
27
31
|
generateText: aiMocks.generateText,
|
|
@@ -25,9 +25,13 @@ vi.mock("../config.js", async () => {
|
|
|
25
25
|
};
|
|
26
26
|
});
|
|
27
27
|
|
|
28
|
-
vi.mock("@ai-sdk/openai-compatible", () => ({
|
|
29
|
-
createOpenAICompatible: vi.fn(() => (modelId: string) => ({ modelId })),
|
|
30
|
-
}));
|
|
28
|
+
vi.mock("@ai-sdk/openai-compatible", () => ({
|
|
29
|
+
createOpenAICompatible: vi.fn(() => (modelId: string) => ({ modelId })),
|
|
30
|
+
}));
|
|
31
|
+
|
|
32
|
+
vi.mock("@ai-sdk/anthropic", () => ({
|
|
33
|
+
createAnthropic: vi.fn(() => (modelId: string) => ({ modelId })),
|
|
34
|
+
}));
|
|
31
35
|
|
|
32
36
|
vi.mock("ai", () => ({
|
|
33
37
|
streamText: aiMocks.streamText,
|
package/deepccc-agent/src/cli.ts
CHANGED
|
@@ -51,13 +51,19 @@ interface JsonLine {
|
|
|
51
51
|
[key: string]: unknown;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
function parsePositiveIntegerOption(name: string, value: string): number {
|
|
54
|
+
function parsePositiveIntegerOption(name: string, value: string): number {
|
|
55
55
|
const parsed = Number(value);
|
|
56
56
|
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
57
57
|
throw new Error(`${name} must be a positive integer`);
|
|
58
58
|
}
|
|
59
59
|
return parsed;
|
|
60
|
-
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function parseProviderOption(value: string): "openai" | "anthropic" {
|
|
63
|
+
const normalized = value.trim().toLowerCase();
|
|
64
|
+
if (normalized === "openai" || normalized === "anthropic") return normalized;
|
|
65
|
+
throw new Error(`--provider must be "openai" or "anthropic", received: ${value}`);
|
|
66
|
+
}
|
|
61
67
|
|
|
62
68
|
function parseArgs(argv = process.argv.slice(2)): ParsedArgs {
|
|
63
69
|
const config: ChatSessionConfig = {};
|
|
@@ -72,7 +78,10 @@ function parseArgs(argv = process.argv.slice(2)): ParsedArgs {
|
|
|
72
78
|
for (let i = 0; i < argv.length; i++) {
|
|
73
79
|
const arg = argv[i];
|
|
74
80
|
const next = argv[i + 1];
|
|
75
|
-
if (arg === "--
|
|
81
|
+
if (arg === "--provider" && next !== undefined) {
|
|
82
|
+
config.provider = parseProviderOption(next);
|
|
83
|
+
i++;
|
|
84
|
+
} else if (arg === "--model" && next !== undefined) {
|
|
76
85
|
config.model = next;
|
|
77
86
|
i++;
|
|
78
87
|
} else if (arg === "--effort" && next !== undefined) {
|
|
@@ -131,10 +140,11 @@ function printHelp(appConfig: RuntimeDeps["appConfig"]): void {
|
|
|
131
140
|
"",
|
|
132
141
|
"Usage: deepccc [options]",
|
|
133
142
|
"",
|
|
134
|
-
"Options:",
|
|
135
|
-
` --
|
|
143
|
+
"Options:",
|
|
144
|
+
` --provider <name> API protocol: openai or anthropic (current default ${appConfig.provider})`,
|
|
145
|
+
` --model <name> Model name (current default ${appConfig.model})`,
|
|
136
146
|
` --effort <level> Reasoning effort: none/minimal/low/medium/high/xhigh/max (overrides config.effort)`,
|
|
137
|
-
` --base-url <url>
|
|
147
|
+
` --base-url <url> Provider API base URL (current default ${appConfig.baseURL})`,
|
|
138
148
|
" --api-key <key> API key",
|
|
139
149
|
" --cwd <path> Working directory",
|
|
140
150
|
" --max-steps <n> Optional tool-step limit. Omit for no step limit",
|
|
@@ -375,7 +385,8 @@ async function runRepl(args: ParsedArgs): Promise<void> {
|
|
|
375
385
|
}
|
|
376
386
|
|
|
377
387
|
console.log(`${C.dim}DeepCCC agent${C.reset}`);
|
|
378
|
-
console.log(`${C.dim}
|
|
388
|
+
console.log(`${C.dim}Provider: ${args.config.provider ?? appConfig.provider}${C.reset}`);
|
|
389
|
+
console.log(`${C.dim}Model: ${args.config.model ?? appConfig.model}${C.reset}`);
|
|
379
390
|
console.log(`${C.dim}Directory: ${cwd}${C.reset}`);
|
|
380
391
|
console.log(`${C.dim}Session: ${resolvedSession.sessionId} (${resolvedSession.mode === "new" ? "new" : "resumed"})${C.reset}`);
|
|
381
392
|
console.log(`${C.dim}Type a message to chat. Double Ctrl+C interrupts generation or exits. Type exit to quit.${C.reset}`);
|
|
@@ -2,7 +2,11 @@ import { existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
4
|
|
|
5
|
+
export type DeepCccProvider = "openai" | "anthropic";
|
|
6
|
+
|
|
5
7
|
export interface DeepCccConfig {
|
|
8
|
+
/** API protocol/provider. Defaults to OpenAI-compatible. */
|
|
9
|
+
provider: DeepCccProvider;
|
|
6
10
|
apiKey: string;
|
|
7
11
|
baseURL: string;
|
|
8
12
|
model: string;
|
|
@@ -23,6 +27,7 @@ export const RAW_STREAM_LOGS_DIR = join(DEEPCCC_HOME, "raw-stream-logs");
|
|
|
23
27
|
const CONFIG_PATH = join(DEEPCCC_HOME, "config.json");
|
|
24
28
|
|
|
25
29
|
const DEFAULT_CONFIG: DeepCccConfig = {
|
|
30
|
+
provider: "openai",
|
|
26
31
|
apiKey: "",
|
|
27
32
|
baseURL: "https://api.deepseek.com/v1",
|
|
28
33
|
model: "deepseek-v4-pro",
|
|
@@ -60,6 +65,13 @@ function numberEnv(name: string): number | undefined {
|
|
|
60
65
|
return Number.isFinite(value) && value >= 0 ? value : undefined;
|
|
61
66
|
}
|
|
62
67
|
|
|
68
|
+
export function normalizeDeepCccProvider(value: unknown): DeepCccProvider {
|
|
69
|
+
if (value === undefined || value === null || String(value).trim() === "") return "openai";
|
|
70
|
+
const normalized = String(value).trim().toLowerCase();
|
|
71
|
+
if (normalized === "openai" || normalized === "anthropic") return normalized;
|
|
72
|
+
throw new Error(`DEEPCCC_PROVIDER/provider must be "openai" or "anthropic", received: ${String(value)}`);
|
|
73
|
+
}
|
|
74
|
+
|
|
63
75
|
function loadConfig(): DeepCccConfig {
|
|
64
76
|
const file = readConfigFile();
|
|
65
77
|
const rawLogs: Partial<DeepCccConfig["rawStreamLogs"]> = file.rawStreamLogs && typeof file.rawStreamLogs === "object"
|
|
@@ -67,6 +79,7 @@ function loadConfig(): DeepCccConfig {
|
|
|
67
79
|
: {};
|
|
68
80
|
|
|
69
81
|
return {
|
|
82
|
+
provider: normalizeDeepCccProvider(env("DEEPCCC_PROVIDER") ?? file.provider ?? DEFAULT_CONFIG.provider),
|
|
70
83
|
apiKey: env("DEEPCCC_API_KEY") ?? env("DEEPSEEK_API_KEY") ?? file.apiKey ?? DEFAULT_CONFIG.apiKey,
|
|
71
84
|
baseURL: env("DEEPCCC_BASE_URL") ?? env("DEEPSEEK_BASE_URL") ?? file.baseURL ?? DEFAULT_CONFIG.baseURL,
|
|
72
85
|
model: env("DEEPCCC_MODEL") ?? env("DEEPSEEK_MODEL") ?? file.model ?? DEFAULT_CONFIG.model,
|
|
@@ -4,14 +4,20 @@
|
|
|
4
4
|
* ChatSession 是程序化入口,既可以被 CLI 调用,也可以被其他模块调用。
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
|
|
7
|
+
import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
|
|
8
|
+
import { createAnthropic } from "@ai-sdk/anthropic";
|
|
8
9
|
import { generateText, isLoopFinished, stepCountIs, streamText, type TextStreamPart } from "ai";
|
|
9
10
|
import { existsSync, readFileSync } from "node:fs";
|
|
10
11
|
import { homedir } from "node:os";
|
|
11
12
|
import { join } from "node:path";
|
|
12
13
|
import { fileURLToPath } from "node:url";
|
|
13
14
|
|
|
14
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
config as appConfig,
|
|
17
|
+
normalizeDeepCccProvider,
|
|
18
|
+
RAW_STREAM_LOGS_DIR,
|
|
19
|
+
type DeepCccProvider,
|
|
20
|
+
} from "./config.js";
|
|
15
21
|
import {
|
|
16
22
|
createRawStreamLog,
|
|
17
23
|
type RawStreamLogHandle,
|
|
@@ -168,17 +174,24 @@ export function loadPlatformCommandPrompt(
|
|
|
168
174
|
return "";
|
|
169
175
|
}
|
|
170
176
|
|
|
171
|
-
function normalizeMaxSteps(value: number | undefined): number | undefined {
|
|
177
|
+
function normalizeMaxSteps(value: number | undefined): number | undefined {
|
|
172
178
|
if (value === undefined) return undefined;
|
|
173
179
|
if (!Number.isFinite(value) || !Number.isInteger(value) || value <= 0) {
|
|
174
180
|
throw new Error("maxSteps must be a positive integer when provided");
|
|
175
181
|
}
|
|
176
|
-
return value;
|
|
177
|
-
}
|
|
182
|
+
return value;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function normalizeAnthropicBaseURL(baseURL: string): string {
|
|
186
|
+
const normalized = baseURL.trim().replace(/\/+$/, "");
|
|
187
|
+
return normalized.endsWith("/v1") ? normalized : `${normalized}/v1`;
|
|
188
|
+
}
|
|
178
189
|
|
|
179
|
-
export interface ChatSessionConfig {
|
|
180
|
-
/**
|
|
181
|
-
|
|
190
|
+
export interface ChatSessionConfig {
|
|
191
|
+
/** API protocol/provider. Defaults to DEEPCCC_PROVIDER/config, then openai. */
|
|
192
|
+
provider?: DeepCccProvider;
|
|
193
|
+
/** Provider service base URL. Defaults to DEEPCCC_BASE_URL/config. */
|
|
194
|
+
baseURL?: string;
|
|
182
195
|
/** API key. Defaults to DEEPCCC_API_KEY/config. */
|
|
183
196
|
apiKey?: string;
|
|
184
197
|
/** Model id. Defaults to DEEPCCC_MODEL/config. */
|
|
@@ -252,8 +265,9 @@ interface ChatMessage {
|
|
|
252
265
|
content: string;
|
|
253
266
|
}
|
|
254
267
|
|
|
255
|
-
export class ChatSession {
|
|
256
|
-
private model: any;
|
|
268
|
+
export class ChatSession {
|
|
269
|
+
private model: any;
|
|
270
|
+
private provider: DeepCccProvider;
|
|
257
271
|
private cwd: string;
|
|
258
272
|
private context: BuiltinContextManager;
|
|
259
273
|
private compactionTimeoutMs: number;
|
|
@@ -276,17 +290,26 @@ export class ChatSession {
|
|
|
276
290
|
);
|
|
277
291
|
}
|
|
278
292
|
|
|
279
|
-
const baseURL = overrides.baseURL ?? appConfig.baseURL;
|
|
280
|
-
const modelId = overrides.model ?? appConfig.model;
|
|
281
|
-
this.
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
293
|
+
const baseURL = overrides.baseURL ?? appConfig.baseURL;
|
|
294
|
+
const modelId = overrides.model ?? appConfig.model;
|
|
295
|
+
this.provider = normalizeDeepCccProvider(overrides.provider ?? appConfig.provider);
|
|
296
|
+
this.effort = (overrides.effort ?? appConfig.effort ?? "").trim();
|
|
297
|
+
|
|
298
|
+
if (this.provider === "anthropic") {
|
|
299
|
+
const provider = createAnthropic({
|
|
300
|
+
baseURL: normalizeAnthropicBaseURL(baseURL),
|
|
301
|
+
apiKey,
|
|
302
|
+
});
|
|
303
|
+
this.model = provider(modelId);
|
|
304
|
+
} else {
|
|
305
|
+
const provider = createOpenAICompatible({
|
|
306
|
+
name: "deepccc",
|
|
307
|
+
baseURL,
|
|
308
|
+
apiKey,
|
|
309
|
+
includeUsage: true,
|
|
310
|
+
});
|
|
311
|
+
this.model = provider(modelId);
|
|
312
|
+
}
|
|
290
313
|
this.cwd = options.cwd ?? process.cwd();
|
|
291
314
|
this.maxSteps = normalizeMaxSteps(options.maxSteps);
|
|
292
315
|
this.compactionTimeoutMs = Math.max(1, options.compactionTimeoutMs ?? DEFAULT_COMPACTION_TIMEOUT_MS);
|
|
@@ -393,7 +416,9 @@ export class ChatSession {
|
|
|
393
416
|
abortSignal: signal,
|
|
394
417
|
// DeepSeek OpenAI 兼容接口:providerOptions.deepseek.reasoningEffort
|
|
395
418
|
// 由 @ai-sdk/openai-compatible 自动映射为请求体 reasoning_effort 字段
|
|
396
|
-
...(this.
|
|
419
|
+
...(this.provider === "openai" && this.effort
|
|
420
|
+
? { providerOptions: { deepseek: { reasoningEffort: this.effort } } }
|
|
421
|
+
: {}),
|
|
397
422
|
};
|
|
398
423
|
let stream: AsyncIterable<TextStreamPart<any>>;
|
|
399
424
|
if (appConfig.streaming) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "chatccc",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.247",
|
|
4
4
|
"description": "Feishu bot bridge for Claude Code",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -49,6 +49,7 @@
|
|
|
49
49
|
"postinstall": "node scripts/postinstall-sharp-check.mjs"
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
|
+
"@ai-sdk/anthropic": "^3.0.105",
|
|
52
53
|
"@ai-sdk/openai-compatible": "^2.0.47",
|
|
53
54
|
"@larksuiteoapi/node-sdk": "^1.59.0",
|
|
54
55
|
"@openilink/openilink-sdk-node": "^0.6.0",
|
|
@@ -2,7 +2,7 @@ import { mkdir, mkdtemp, readFile, writeFile } from "node:fs/promises";
|
|
|
2
2
|
import { tmpdir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
|
|
5
|
-
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
6
6
|
|
|
7
7
|
import { config } from "../../deepccc-agent/src/config.ts";
|
|
8
8
|
import { estimateBuiltinContextTokens } from "../../deepccc-agent/src/context.ts";
|
|
@@ -13,11 +13,16 @@ const createRawStreamLogMock = vi.fn();
|
|
|
13
13
|
const rawLogWriteLineMock = vi.fn();
|
|
14
14
|
const rawLogCloseMock = vi.fn();
|
|
15
15
|
const originalRawStreamLogs = structuredClone(config.rawStreamLogs);
|
|
16
|
+
const originalProvider = config.provider;
|
|
16
17
|
|
|
17
18
|
vi.mock("@ai-sdk/openai-compatible", () => ({
|
|
18
19
|
createOpenAICompatible: vi.fn(() => (modelId: string) => ({ modelId })),
|
|
19
20
|
}));
|
|
20
21
|
|
|
22
|
+
vi.mock("@ai-sdk/anthropic", () => ({
|
|
23
|
+
createAnthropic: vi.fn(() => (modelId: string) => ({ modelId })),
|
|
24
|
+
}));
|
|
25
|
+
|
|
21
26
|
vi.mock("ai", () => ({
|
|
22
27
|
streamText: streamTextMock,
|
|
23
28
|
generateText: generateTextMock,
|
|
@@ -45,6 +50,10 @@ async function* fullStream(...parts: unknown[]): AsyncIterable<unknown> {
|
|
|
45
50
|
for (const part of parts) yield part;
|
|
46
51
|
}
|
|
47
52
|
|
|
53
|
+
beforeEach(() => {
|
|
54
|
+
config.provider = "openai";
|
|
55
|
+
});
|
|
56
|
+
|
|
48
57
|
afterEach(() => {
|
|
49
58
|
streamTextMock.mockReset();
|
|
50
59
|
generateTextMock.mockReset();
|
|
@@ -52,6 +61,7 @@ afterEach(() => {
|
|
|
52
61
|
rawLogWriteLineMock.mockReset();
|
|
53
62
|
rawLogCloseMock.mockReset();
|
|
54
63
|
config.rawStreamLogs = structuredClone(originalRawStreamLogs);
|
|
64
|
+
config.provider = originalProvider;
|
|
55
65
|
vi.useRealTimers();
|
|
56
66
|
});
|
|
57
67
|
|
|
@@ -22,6 +22,10 @@ vi.mock("@ai-sdk/openai-compatible", () => ({
|
|
|
22
22
|
createOpenAICompatible: vi.fn(() => (modelId: string) => ({ modelId })),
|
|
23
23
|
}));
|
|
24
24
|
|
|
25
|
+
vi.mock("@ai-sdk/anthropic", () => ({
|
|
26
|
+
createAnthropic: vi.fn(() => (modelId: string) => ({ modelId })),
|
|
27
|
+
}));
|
|
28
|
+
|
|
25
29
|
vi.mock("ai", () => ({
|
|
26
30
|
streamText: aiMocks.streamText,
|
|
27
31
|
generateText: aiMocks.generateText,
|
|
@@ -170,7 +174,7 @@ describe("builtin file-tools permission integration", () => {
|
|
|
170
174
|
describe("builtin ChatSession permission integration", () => {
|
|
171
175
|
it("permissionMode bypass lets high-risk run_command execute", async () => {
|
|
172
176
|
const session = new ChatSession(
|
|
173
|
-
{ apiKey: "sk-test" },
|
|
177
|
+
{ apiKey: "sk-test", provider: "openai" },
|
|
174
178
|
{ sessionId: "perm-bypass", permissionMode: "bypass" },
|
|
175
179
|
);
|
|
176
180
|
streamTextMock.mockReturnValueOnce({ textStream: textStream("hi") });
|
|
@@ -182,7 +186,7 @@ describe("builtin ChatSession permission integration", () => {
|
|
|
182
186
|
|
|
183
187
|
it("default ask mode without resolver denies high-risk run_command", async () => {
|
|
184
188
|
const session = new ChatSession(
|
|
185
|
-
{ apiKey: "sk-test" },
|
|
189
|
+
{ apiKey: "sk-test", provider: "openai" },
|
|
186
190
|
{ sessionId: "perm-ask" },
|
|
187
191
|
);
|
|
188
192
|
streamTextMock.mockReturnValueOnce({ textStream: textStream("hi") });
|
|
@@ -197,7 +201,11 @@ describe("builtin ChatSession permission integration", () => {
|
|
|
197
201
|
describe("ccc-adapter uses bypass mode (aligns with claude/codex)", () => {
|
|
198
202
|
it("tools created via ccc adapter run high-risk commands without asking", async () => {
|
|
199
203
|
const { createCccAdapter } = await import("../adapters/ccc-adapter.ts");
|
|
200
|
-
const adapter = createCccAdapter({
|
|
204
|
+
const adapter = createCccAdapter({
|
|
205
|
+
apiKey: "sk-test",
|
|
206
|
+
provider: "openai",
|
|
207
|
+
contextDir: testHome.dir,
|
|
208
|
+
});
|
|
201
209
|
const { sessionId } = await adapter.createSession(testHome.dir);
|
|
202
210
|
streamTextMock.mockReturnValueOnce({ textStream: textStream("hi") });
|
|
203
211
|
|
|
@@ -11,6 +11,10 @@ vi.mock("@ai-sdk/openai-compatible", () => ({
|
|
|
11
11
|
createOpenAICompatible: vi.fn(() => (modelId: string) => ({ modelId })),
|
|
12
12
|
}));
|
|
13
13
|
|
|
14
|
+
vi.mock("@ai-sdk/anthropic", () => ({
|
|
15
|
+
createAnthropic: vi.fn(() => (modelId: string) => ({ modelId })),
|
|
16
|
+
}));
|
|
17
|
+
|
|
14
18
|
vi.mock("ai", () => ({
|
|
15
19
|
streamText: streamTextMock,
|
|
16
20
|
generateText: generateTextMock,
|
|
@@ -54,6 +58,7 @@ describe("createCccAdapter", () => {
|
|
|
54
58
|
const contextDir = await mkdtemp(join(tmpdir(), "chatccc-ccc-adapter-meta-"));
|
|
55
59
|
const adapter = createCccAdapter({
|
|
56
60
|
apiKey: "sk-test",
|
|
61
|
+
provider: "openai",
|
|
57
62
|
contextDir,
|
|
58
63
|
model: "deepseek-v4-pro",
|
|
59
64
|
});
|
|
@@ -74,6 +79,7 @@ describe("createCccAdapter", () => {
|
|
|
74
79
|
const contextDir = await mkdtemp(join(tmpdir(), "chatccc-ccc-adapter-stream-"));
|
|
75
80
|
const adapter = createCccAdapter({
|
|
76
81
|
apiKey: "sk-test",
|
|
82
|
+
provider: "openai",
|
|
77
83
|
contextDir,
|
|
78
84
|
model: "deepseek-v4-flash",
|
|
79
85
|
});
|
|
@@ -101,6 +107,7 @@ describe("createCccAdapter", () => {
|
|
|
101
107
|
const contextDir = await mkdtemp(join(tmpdir(), "chatccc-ccc-adapter-tools-"));
|
|
102
108
|
const adapter = createCccAdapter({
|
|
103
109
|
apiKey: "sk-test",
|
|
110
|
+
provider: "openai",
|
|
104
111
|
contextDir,
|
|
105
112
|
model: "deepseek-v4-flash",
|
|
106
113
|
});
|
|
@@ -136,6 +143,7 @@ describe("createCccAdapter", () => {
|
|
|
136
143
|
const contextDir = await mkdtemp(join(tmpdir(), "chatccc-ccc-adapter-status-"));
|
|
137
144
|
const adapter = createCccAdapter({
|
|
138
145
|
apiKey: "sk-test",
|
|
146
|
+
provider: "openai",
|
|
139
147
|
contextDir,
|
|
140
148
|
compactAtTokens: 1,
|
|
141
149
|
keepRecentMessages: 1,
|
|
@@ -168,6 +176,7 @@ describe("createCccAdapter", () => {
|
|
|
168
176
|
const contextDir = await mkdtemp(join(tmpdir(), "chatccc-ccc-adapter-effort-"));
|
|
169
177
|
const adapter = createCccAdapter({
|
|
170
178
|
apiKey: "sk-test",
|
|
179
|
+
provider: "openai",
|
|
171
180
|
contextDir,
|
|
172
181
|
effort: "xhigh",
|
|
173
182
|
});
|
|
@@ -72,7 +72,7 @@ const baseAppConfig: AppConfig = {
|
|
|
72
72
|
onDemandMonthlyBudget: 1000,
|
|
73
73
|
},
|
|
74
74
|
codex: { enabled: true, defaultAgent: false, path: "/initial/codex", model: "initial-codex-model", alternativeModel: "initial-codex-alt-model", effort: "initial-codex-effort", fastMode: false },
|
|
75
|
-
ccc: { enabled: true, defaultAgent: false, DEEPSEEK_API_KEY: "initial-ccc-key", DEEPSEEK_BASE_URL: "https://initial.deepseek.test/v1", model: "initial-ccc-model", alternativeModel: "initial-ccc-alt-model", effort: "initial-ccc-effort" },
|
|
75
|
+
ccc: { enabled: true, defaultAgent: false, DEEPSEEK_API_KEY: "initial-ccc-key", DEEPSEEK_BASE_URL: "https://initial.deepseek.test/v1", model: "initial-ccc-model", alternativeModel: "initial-ccc-alt-model", effort: "initial-ccc-effort", provider: "" },
|
|
76
76
|
};
|
|
77
77
|
|
|
78
78
|
// 把 module 状态抢救快照:每个 it 跑前重置回这个状态,避免污染相邻测试。
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { normalizeCccProviderOverride } from "../config-utils.ts";
|
|
3
|
+
|
|
4
|
+
describe("normalizeCccProviderOverride", () => {
|
|
5
|
+
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
6
|
+
|
|
7
|
+
afterEach(() => {
|
|
8
|
+
warnSpy.mockClear();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it("returns empty string for missing / non-string values (no override)", () => {
|
|
12
|
+
expect(normalizeCccProviderOverride(undefined)).toBe("");
|
|
13
|
+
expect(normalizeCccProviderOverride(null)).toBe("");
|
|
14
|
+
expect(normalizeCccProviderOverride(42)).toBe("");
|
|
15
|
+
expect(normalizeCccProviderOverride({})).toBe("");
|
|
16
|
+
expect(warnSpy).not.toHaveBeenCalled();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("treats empty/whitespace and legacy 'default' as no override", () => {
|
|
20
|
+
expect(normalizeCccProviderOverride("")).toBe("");
|
|
21
|
+
expect(normalizeCccProviderOverride(" ")).toBe("");
|
|
22
|
+
expect(normalizeCccProviderOverride("default")).toBe("");
|
|
23
|
+
expect(normalizeCccProviderOverride("DEFAULT")).toBe("");
|
|
24
|
+
expect(warnSpy).not.toHaveBeenCalled();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("normalizes openai / anthropic case-insensitively", () => {
|
|
28
|
+
expect(normalizeCccProviderOverride("openai")).toBe("openai");
|
|
29
|
+
expect(normalizeCccProviderOverride("OPENAI")).toBe("openai");
|
|
30
|
+
expect(normalizeCccProviderOverride(" anthropic ")).toBe("anthropic");
|
|
31
|
+
expect(normalizeCccProviderOverride("Anthropic")).toBe("anthropic");
|
|
32
|
+
expect(warnSpy).not.toHaveBeenCalled();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("ignores invalid values with a warning", () => {
|
|
36
|
+
expect(normalizeCccProviderOverride("gemini")).toBe("");
|
|
37
|
+
expect(normalizeCccProviderOverride("oai")).toBe("");
|
|
38
|
+
expect(warnSpy).toHaveBeenCalledTimes(2);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
@@ -31,10 +31,12 @@ describe("CCC Agent ChatCCC configuration", () => {
|
|
|
31
31
|
DEEPSEEK_BASE_URL: "https://chatccc.example.com/v1",
|
|
32
32
|
model: "chatccc-model",
|
|
33
33
|
effort: "high",
|
|
34
|
+
provider: "",
|
|
34
35
|
});
|
|
35
36
|
|
|
36
37
|
getAdapterForTool("ccc");
|
|
37
38
|
|
|
39
|
+
// provider 留空时不传 → ChatSession 跟随 DeepCCC 内核配置(~/.deepccc/config.json 或 DEEPCCC_PROVIDER)
|
|
38
40
|
expect(createCccAdapterMock).toHaveBeenCalledWith({
|
|
39
41
|
apiKey: "chatccc-api-key",
|
|
40
42
|
baseURL: "https://chatccc.example.com/v1",
|
|
@@ -42,4 +44,23 @@ describe("CCC Agent ChatCCC configuration", () => {
|
|
|
42
44
|
effort: "high",
|
|
43
45
|
});
|
|
44
46
|
});
|
|
47
|
+
|
|
48
|
+
it("forwards ccc.provider override to createCccAdapter", () => {
|
|
49
|
+
Object.assign(config.ccc, {
|
|
50
|
+
DEEPSEEK_API_KEY: "chatccc-api-key",
|
|
51
|
+
DEEPSEEK_BASE_URL: "https://chatccc.example.com/v1",
|
|
52
|
+
model: "chatccc-model",
|
|
53
|
+
effort: "",
|
|
54
|
+
provider: "anthropic",
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
getAdapterForTool("ccc");
|
|
58
|
+
|
|
59
|
+
expect(createCccAdapterMock).toHaveBeenCalledWith({
|
|
60
|
+
apiKey: "chatccc-api-key",
|
|
61
|
+
baseURL: "https://chatccc.example.com/v1",
|
|
62
|
+
model: "chatccc-model",
|
|
63
|
+
provider: "anthropic",
|
|
64
|
+
});
|
|
65
|
+
});
|
|
45
66
|
});
|
|
@@ -107,6 +107,7 @@ describe("unflattenConfig", () => {
|
|
|
107
107
|
CHATCCC_CCC_MODEL: "deepseek-v4-flash",
|
|
108
108
|
CHATCCC_CCC_ALTERNATIVE_MODEL: "deepseek-v4-pro",
|
|
109
109
|
CHATCCC_CCC_EFFORT: "max",
|
|
110
|
+
CHATCCC_CCC_PROVIDER: "anthropic",
|
|
110
111
|
}),
|
|
111
112
|
).toEqual({
|
|
112
113
|
ccc: {
|
|
@@ -117,6 +118,7 @@ describe("unflattenConfig", () => {
|
|
|
117
118
|
model: "deepseek-v4-flash",
|
|
118
119
|
alternativeModel: "deepseek-v4-pro",
|
|
119
120
|
effort: "max",
|
|
121
|
+
provider: "anthropic",
|
|
120
122
|
},
|
|
121
123
|
});
|
|
122
124
|
});
|
|
@@ -49,6 +49,7 @@ export function createCccAdapter(options: CccAdapterOptions = {}): ToolAdapter {
|
|
|
49
49
|
|
|
50
50
|
const chatConfig: ChatSessionConfig = {
|
|
51
51
|
apiKey: options.apiKey,
|
|
52
|
+
...(options.provider !== undefined ? { provider: options.provider } : {}),
|
|
52
53
|
...(options.baseURL !== undefined ? { baseURL: options.baseURL } : {}),
|
|
53
54
|
...(options.model !== undefined ? { model: options.model } : {}),
|
|
54
55
|
...(options.effort !== undefined ? { effort: options.effort } : {}),
|