chatccc 0.2.247 → 0.2.248

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.
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "deepccc",
3
- "version": "0.1.16",
3
+ "version": "0.1.17",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "deepccc",
9
- "version": "0.1.16",
9
+ "version": "0.1.17",
10
10
  "license": "Apache-2.0",
11
11
  "dependencies": {
12
12
  "@ai-sdk/anthropic": "^3.0.105",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deepccc",
3
- "version": "0.1.16",
3
+ "version": "0.1.17",
4
4
  "description": "A lightweight coding agent with OpenAI-compatible and Anthropic Messages API support.",
5
5
  "license": "Apache-2.0",
6
6
  "keywords": [
@@ -16,6 +16,7 @@ const rawLogCloseMock = vi.fn();
16
16
  const originalRawStreamLogs = structuredClone(config.rawStreamLogs);
17
17
  const originalStreaming = config.streaming;
18
18
  const originalProvider = config.provider;
19
+ const originalEffort = config.effort;
19
20
  const createOpenAICompatibleMock = vi.fn(() => (modelId: string) => ({ modelId }));
20
21
  const createAnthropicMock = vi.fn(() => (modelId: string) => ({ modelId, provider: "anthropic" }));
21
22
 
@@ -57,6 +58,7 @@ async function* fullStream(...parts: unknown[]): AsyncIterable<unknown> {
57
58
  beforeEach(() => {
58
59
  config.provider = "openai";
59
60
  config.streaming = true;
61
+ config.effort = "";
60
62
  });
61
63
 
62
64
  afterEach(() => {
@@ -68,6 +70,7 @@ afterEach(() => {
68
70
  config.rawStreamLogs = structuredClone(originalRawStreamLogs);
69
71
  config.provider = originalProvider;
70
72
  config.streaming = originalStreaming;
73
+ config.effort = originalEffort;
71
74
  createOpenAICompatibleMock.mockClear();
72
75
  createAnthropicMock.mockClear();
73
76
  vi.useRealTimers();
@@ -103,7 +106,7 @@ describe("ChatSession response transport", () => {
103
106
  expect(createOpenAICompatibleMock).not.toHaveBeenCalled();
104
107
  });
105
108
 
106
- it("keeps an existing /v1 suffix for Anthropic and streams without OpenAI-only effort options", async () => {
109
+ it("keeps an existing /v1 suffix and maps Anthropic effort to output_config.effort", async () => {
107
110
  const { ChatSession } = await import("../index.js");
108
111
  streamTextMock.mockReturnValueOnce({ textStream: textStream("done") });
109
112
  const session = new ChatSession({
@@ -120,10 +123,47 @@ describe("ChatSession response transport", () => {
120
123
  baseURL: "https://gateway.example/v1",
121
124
  apiKey: "sk-test",
122
125
  });
126
+ expect(streamTextMock).toHaveBeenCalledOnce();
127
+ expect(streamTextMock.mock.calls[0]?.[0]).toMatchObject({
128
+ providerOptions: { anthropic: { effort: "high" } },
129
+ });
130
+ });
131
+
132
+ it("omits providerOptions when effort is empty for the Anthropic protocol", async () => {
133
+ const { ChatSession } = await import("../index.js");
134
+ streamTextMock.mockReturnValueOnce({ textStream: textStream("done") });
135
+ const session = new ChatSession({
136
+ provider: "anthropic",
137
+ apiKey: "sk-test",
138
+ baseURL: "https://gateway.example",
139
+ model: "model-a",
140
+ });
141
+
142
+ await collect(session.chat("hello"));
143
+
123
144
  expect(streamTextMock).toHaveBeenCalledOnce();
124
145
  expect(streamTextMock.mock.calls[0]?.[0]).not.toHaveProperty("providerOptions");
125
146
  });
126
147
 
148
+ it("maps OpenAI-compatible effort to DeepSeek reasoningEffort", async () => {
149
+ const { ChatSession } = await import("../index.js");
150
+ streamTextMock.mockReturnValueOnce({ textStream: textStream("done") });
151
+ const session = new ChatSession({
152
+ provider: "openai",
153
+ apiKey: "sk-test",
154
+ baseURL: "https://gateway.example",
155
+ model: "model-a",
156
+ effort: "max",
157
+ });
158
+
159
+ await collect(session.chat("hello"));
160
+
161
+ expect(streamTextMock).toHaveBeenCalledOnce();
162
+ expect(streamTextMock.mock.calls[0]?.[0]).toMatchObject({
163
+ providerOptions: { deepseek: { reasoningEffort: "max" } },
164
+ });
165
+ });
166
+
127
167
  it("asks the provider to include usage in streaming responses", async () => {
128
168
  const { ChatSession } = await import("../index.js");
129
169
 
@@ -415,7 +455,10 @@ describe("ChatSession context management", () => {
415
455
  const events = await collect(restored.chat("new question"));
416
456
 
417
457
  expect(generateTextMock).toHaveBeenCalledOnce();
418
- expect(generateTextMock).toHaveBeenCalledWith(expect.objectContaining({ temperature: 0 }));
458
+ expect(generateTextMock).toHaveBeenCalledWith(expect.objectContaining({
459
+ temperature: 0,
460
+ providerOptions: { deepseek: { reasoningEffort: "none" } },
461
+ }));
419
462
  expect(streamTextMock).toHaveBeenLastCalledWith(expect.objectContaining({
420
463
  messages: expect.arrayContaining([
421
464
  expect.objectContaining({ content: expect.stringContaining("old question summarized") }),
@@ -430,6 +473,38 @@ describe("ChatSession context management", () => {
430
473
  expect(restored.history.map((m) => m.content).join("\n")).toContain("new answer");
431
474
  });
432
475
 
476
+ it("locks compaction to low effort under the Anthropic protocol", async () => {
477
+ const { ChatSession } = await import("../index.js");
478
+ const dir = await mkdtemp(join(tmpdir(), "deepccc-session-compaction-anthropic-effort-"));
479
+ const base = { apiKey: "sk-test", provider: "anthropic" as const, baseURL: "https://gateway.example", model: "model-a" };
480
+
481
+ const seed = new ChatSession(base, {
482
+ persist: true,
483
+ contextDir: dir,
484
+ sessionId: "compaction-anthropic-effort",
485
+ compactAtTokens: 10_000,
486
+ });
487
+ streamTextMock.mockReturnValueOnce({ textStream: textStream("old answer") });
488
+ await collect(seed.chat("old question"));
489
+
490
+ generateTextMock.mockResolvedValueOnce({ text: "## Current Task\n- old question summarized" });
491
+ streamTextMock.mockReturnValueOnce({ textStream: textStream("new answer") });
492
+
493
+ const restored = new ChatSession(base, {
494
+ persist: true,
495
+ contextDir: dir,
496
+ sessionId: "compaction-anthropic-effort",
497
+ compactAtTokens: 1,
498
+ keepRecentMessages: 1,
499
+ });
500
+ await collect(restored.chat("new question"));
501
+
502
+ expect(generateTextMock).toHaveBeenCalledOnce();
503
+ expect(generateTextMock).toHaveBeenCalledWith(expect.objectContaining({
504
+ providerOptions: { anthropic: { effort: "low" } },
505
+ }));
506
+ });
507
+
433
508
  it("times out context compaction independently before reply generation", async () => {
434
509
  vi.useFakeTimers();
435
510
  const { ChatSession } = await import("../index.js");
@@ -4,20 +4,21 @@
4
4
  * ChatSession 是程序化入口,既可以被 CLI 调用,也可以被其他模块调用。
5
5
  */
6
6
 
7
- import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
8
- import { createAnthropic } from "@ai-sdk/anthropic";
7
+ import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
8
+ import { createAnthropic } from "@ai-sdk/anthropic";
9
+ import type { JSONObject } from "@ai-sdk/provider";
9
10
  import { generateText, isLoopFinished, stepCountIs, streamText, type TextStreamPart } from "ai";
10
11
  import { existsSync, readFileSync } from "node:fs";
11
12
  import { homedir } from "node:os";
12
13
  import { join } from "node:path";
13
14
  import { fileURLToPath } from "node:url";
14
15
 
15
- import {
16
- config as appConfig,
17
- normalizeDeepCccProvider,
18
- RAW_STREAM_LOGS_DIR,
19
- type DeepCccProvider,
20
- } from "./config.js";
16
+ import {
17
+ config as appConfig,
18
+ normalizeDeepCccProvider,
19
+ RAW_STREAM_LOGS_DIR,
20
+ type DeepCccProvider,
21
+ } from "./config.js";
21
22
  import {
22
23
  createRawStreamLog,
23
24
  type RawStreamLogHandle,
@@ -174,24 +175,24 @@ export function loadPlatformCommandPrompt(
174
175
  return "";
175
176
  }
176
177
 
177
- function normalizeMaxSteps(value: number | undefined): number | undefined {
178
+ function normalizeMaxSteps(value: number | undefined): number | undefined {
178
179
  if (value === undefined) return undefined;
179
180
  if (!Number.isFinite(value) || !Number.isInteger(value) || value <= 0) {
180
181
  throw new Error("maxSteps must be a positive integer when provided");
181
182
  }
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
- }
189
-
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;
183
+ return value;
184
+ }
185
+
186
+ function normalizeAnthropicBaseURL(baseURL: string): string {
187
+ const normalized = baseURL.trim().replace(/\/+$/, "");
188
+ return normalized.endsWith("/v1") ? normalized : `${normalized}/v1`;
189
+ }
190
+
191
+ export interface ChatSessionConfig {
192
+ /** API protocol/provider. Defaults to DEEPCCC_PROVIDER/config, then openai. */
193
+ provider?: DeepCccProvider;
194
+ /** Provider service base URL. Defaults to DEEPCCC_BASE_URL/config. */
195
+ baseURL?: string;
195
196
  /** API key. Defaults to DEEPCCC_API_KEY/config. */
196
197
  apiKey?: string;
197
198
  /** Model id. Defaults to DEEPCCC_MODEL/config. */
@@ -265,9 +266,9 @@ interface ChatMessage {
265
266
  content: string;
266
267
  }
267
268
 
268
- export class ChatSession {
269
- private model: any;
270
- private provider: DeepCccProvider;
269
+ export class ChatSession {
270
+ private model: any;
271
+ private provider: DeepCccProvider;
271
272
  private cwd: string;
272
273
  private context: BuiltinContextManager;
273
274
  private compactionTimeoutMs: number;
@@ -290,26 +291,26 @@ export class ChatSession {
290
291
  );
291
292
  }
292
293
 
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
- }
294
+ const baseURL = overrides.baseURL ?? appConfig.baseURL;
295
+ const modelId = overrides.model ?? appConfig.model;
296
+ this.provider = normalizeDeepCccProvider(overrides.provider ?? appConfig.provider);
297
+ this.effort = (overrides.effort ?? appConfig.effort ?? "").trim();
298
+
299
+ if (this.provider === "anthropic") {
300
+ const provider = createAnthropic({
301
+ baseURL: normalizeAnthropicBaseURL(baseURL),
302
+ apiKey,
303
+ });
304
+ this.model = provider(modelId);
305
+ } else {
306
+ const provider = createOpenAICompatible({
307
+ name: "deepccc",
308
+ baseURL,
309
+ apiKey,
310
+ includeUsage: true,
311
+ });
312
+ this.model = provider(modelId);
313
+ }
313
314
  this.cwd = options.cwd ?? process.cwd();
314
315
  this.maxSteps = normalizeMaxSteps(options.maxSteps);
315
316
  this.compactionTimeoutMs = Math.max(1, options.compactionTimeoutMs ?? DEFAULT_COMPACTION_TIMEOUT_MS);
@@ -407,29 +408,36 @@ export class ChatSession {
407
408
  const skills = await scanSkillsDirs(this.skillDirs);
408
409
  const system = this.buildSystemPrompt(skills);
409
410
  this.systemPrompt = system;
410
- const generationOptions = {
411
- model: this.model,
412
- system,
413
- messages: this.context.buildModelMessages() as any,
411
+ // effort 按协议映射:
412
+ // - OpenAI 兼容:providerOptions.deepseek.reasoningEffort 由 @ai-sdk/openai-compatible
413
+ // 自动映射为请求体 reasoning_effort 字段(DeepSeek 原生支持);
414
+ // - Anthropic:providerOptions.anthropic.effort @ai-sdk/anthropic 组装为请求体
415
+ // output_config.effort(官方 Effort API,见 platform.claude.com/docs/en/build-with-claude/effort)
416
+ let effortProviderOptions: Record<string, JSONObject> | undefined;
417
+ if (this.effort) {
418
+ effortProviderOptions = this.provider === "openai"
419
+ ? { deepseek: { reasoningEffort: this.effort } }
420
+ : { anthropic: { effort: this.effort } };
421
+ }
422
+ const generationOptions = {
423
+ model: this.model,
424
+ system,
425
+ messages: this.context.buildModelMessages() as any,
414
426
  tools: createBuiltinFileTools(this.cwd, { permissionGate: this.permissionGate }),
415
427
  stopWhen: maxSteps !== undefined ? stepCountIs(maxSteps) : isLoopFinished(),
416
428
  abortSignal: signal,
417
- // DeepSeek OpenAI 兼容接口:providerOptions.deepseek.reasoningEffort
418
- // 由 @ai-sdk/openai-compatible 自动映射为请求体 reasoning_effort 字段
419
- ...(this.provider === "openai" && this.effort
420
- ? { providerOptions: { deepseek: { reasoningEffort: this.effort } } }
421
- : {}),
422
- };
423
- let stream: AsyncIterable<TextStreamPart<any>>;
424
- if (appConfig.streaming) {
425
- const result = streamText(generationOptions);
426
- stream = result.fullStream ?? textStreamToFullStream(result.textStream);
427
- } else {
428
- const result = await generateText(generationOptions);
429
- stream = generateResultToFullStream(result);
430
- }
431
-
432
- for await (const part of stream as AsyncIterable<TextStreamPart<any>>) {
429
+ ...(effortProviderOptions ? { providerOptions: effortProviderOptions } : {}),
430
+ };
431
+ let stream: AsyncIterable<TextStreamPart<any>>;
432
+ if (appConfig.streaming) {
433
+ const result = streamText(generationOptions);
434
+ stream = result.fullStream ?? textStreamToFullStream(result.textStream);
435
+ } else {
436
+ const result = await generateText(generationOptions);
437
+ stream = generateResultToFullStream(result);
438
+ }
439
+
440
+ for await (const part of stream as AsyncIterable<TextStreamPart<any>>) {
433
441
  rawLog?.writeLine(safeRawStreamJson(part));
434
442
  if (part.type === "text-delta") {
435
443
  fullText += part.text;
@@ -562,6 +570,11 @@ export class ChatSession {
562
570
  messages: [{ role: "user", content: buildSummaryPrompt(plan) }],
563
571
  abortSignal: compactionSignal,
564
572
  temperature: 0,
573
+ // 压缩是摘要类任务:显式锁低 effort(OpenAI reasoning_effort=none / Anthropic
574
+ // output_config.effort=low),避免继承主对话的高 effort 拖慢"压缩上下文中"阶段
575
+ providerOptions: this.provider === "openai"
576
+ ? { deepseek: { reasoningEffort: "none" } }
577
+ : { anthropic: { effort: "low" } },
565
578
  });
566
579
 
567
580
  if (!result.text.trim()) {
@@ -587,40 +600,40 @@ export class ChatSession {
587
600
  }
588
601
  }
589
602
 
590
- async function* textStreamToFullStream(stream: AsyncIterable<string>): AsyncIterable<{ type: "text-delta"; text: string }> {
591
- for await (const text of stream) {
592
- yield { type: "text-delta", text };
593
- }
594
- }
595
-
596
- async function* generateResultToFullStream(result: any): AsyncIterable<TextStreamPart<any>> {
597
- let emittedText = false;
598
- for (const step of result.steps ?? []) {
599
- for (const call of step.toolCalls ?? []) {
600
- yield {
601
- type: "tool-call",
602
- toolCallId: call.toolCallId,
603
- toolName: call.toolName,
604
- input: call.input,
605
- } as TextStreamPart<any>;
606
- }
607
- for (const toolResult of step.toolResults ?? []) {
608
- yield {
609
- type: "tool-result",
610
- toolCallId: toolResult.toolCallId,
611
- toolName: toolResult.toolName,
612
- output: toolResult.output,
613
- } as TextStreamPart<any>;
614
- }
615
- if (step.text) {
616
- emittedText = true;
617
- yield { type: "text-delta", text: step.text } as TextStreamPart<any>;
618
- }
619
- }
620
- if (!emittedText && result.text) {
621
- yield { type: "text-delta", text: result.text } as TextStreamPart<any>;
622
- }
623
- }
603
+ async function* textStreamToFullStream(stream: AsyncIterable<string>): AsyncIterable<{ type: "text-delta"; text: string }> {
604
+ for await (const text of stream) {
605
+ yield { type: "text-delta", text };
606
+ }
607
+ }
608
+
609
+ async function* generateResultToFullStream(result: any): AsyncIterable<TextStreamPart<any>> {
610
+ let emittedText = false;
611
+ for (const step of result.steps ?? []) {
612
+ for (const call of step.toolCalls ?? []) {
613
+ yield {
614
+ type: "tool-call",
615
+ toolCallId: call.toolCallId,
616
+ toolName: call.toolName,
617
+ input: call.input,
618
+ } as TextStreamPart<any>;
619
+ }
620
+ for (const toolResult of step.toolResults ?? []) {
621
+ yield {
622
+ type: "tool-result",
623
+ toolCallId: toolResult.toolCallId,
624
+ toolName: toolResult.toolName,
625
+ output: toolResult.output,
626
+ } as TextStreamPart<any>;
627
+ }
628
+ if (step.text) {
629
+ emittedText = true;
630
+ yield { type: "text-delta", text: step.text } as TextStreamPart<any>;
631
+ }
632
+ }
633
+ if (!emittedText && result.text) {
634
+ yield { type: "text-delta", text: result.text } as TextStreamPart<any>;
635
+ }
636
+ }
624
637
 
625
638
  function safeJson(value: unknown): string {
626
639
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chatccc",
3
- "version": "0.2.247",
3
+ "version": "0.2.248",
4
4
  "description": "Feishu bot bridge for Claude Code",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",