thatgfsj-code 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (165) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +136 -0
  3. package/ROADMAP.md +61 -0
  4. package/dist/app/agent.d.ts +31 -0
  5. package/dist/app/agent.d.ts.map +1 -0
  6. package/dist/app/agent.js +106 -0
  7. package/dist/app/agent.js.map +1 -0
  8. package/dist/app/index.d.ts +39 -0
  9. package/dist/app/index.d.ts.map +1 -0
  10. package/dist/app/index.js +80 -0
  11. package/dist/app/index.js.map +1 -0
  12. package/dist/cmd/index.d.ts +12 -0
  13. package/dist/cmd/index.d.ts.map +1 -0
  14. package/dist/cmd/index.js +85 -0
  15. package/dist/cmd/index.js.map +1 -0
  16. package/dist/config/index.d.ts +39 -0
  17. package/dist/config/index.d.ts.map +1 -0
  18. package/dist/config/index.js +110 -0
  19. package/dist/config/index.js.map +1 -0
  20. package/dist/config/providers.d.ts +23 -0
  21. package/dist/config/providers.d.ts.map +1 -0
  22. package/dist/config/providers.js +152 -0
  23. package/dist/config/providers.js.map +1 -0
  24. package/dist/config/types.d.ts +33 -0
  25. package/dist/config/types.d.ts.map +1 -0
  26. package/dist/config/types.js +5 -0
  27. package/dist/config/types.js.map +1 -0
  28. package/dist/hooks/index.d.ts +31 -0
  29. package/dist/hooks/index.d.ts.map +1 -0
  30. package/dist/hooks/index.js +71 -0
  31. package/dist/hooks/index.js.map +1 -0
  32. package/dist/llm/anthropic.d.ts +17 -0
  33. package/dist/llm/anthropic.d.ts.map +1 -0
  34. package/dist/llm/anthropic.js +126 -0
  35. package/dist/llm/anthropic.js.map +1 -0
  36. package/dist/llm/gemini.d.ts +16 -0
  37. package/dist/llm/gemini.d.ts.map +1 -0
  38. package/dist/llm/gemini.js +107 -0
  39. package/dist/llm/gemini.js.map +1 -0
  40. package/dist/llm/index.d.ts +48 -0
  41. package/dist/llm/index.d.ts.map +1 -0
  42. package/dist/llm/index.js +174 -0
  43. package/dist/llm/index.js.map +1 -0
  44. package/dist/llm/openai.d.ts +17 -0
  45. package/dist/llm/openai.d.ts.map +1 -0
  46. package/dist/llm/openai.js +113 -0
  47. package/dist/llm/openai.js.map +1 -0
  48. package/dist/llm/provider.d.ts +33 -0
  49. package/dist/llm/provider.d.ts.map +1 -0
  50. package/dist/llm/provider.js +6 -0
  51. package/dist/llm/provider.js.map +1 -0
  52. package/dist/mcp/client.d.ts +83 -0
  53. package/dist/mcp/client.d.ts.map +1 -0
  54. package/dist/mcp/client.js +267 -0
  55. package/dist/mcp/client.js.map +1 -0
  56. package/dist/prompts/index.d.ts +27 -0
  57. package/dist/prompts/index.d.ts.map +1 -0
  58. package/dist/prompts/index.js +102 -0
  59. package/dist/prompts/index.js.map +1 -0
  60. package/dist/session/compactor.d.ts +48 -0
  61. package/dist/session/compactor.d.ts.map +1 -0
  62. package/dist/session/compactor.js +77 -0
  63. package/dist/session/compactor.js.map +1 -0
  64. package/dist/session/index.d.ts +48 -0
  65. package/dist/session/index.d.ts.map +1 -0
  66. package/dist/session/index.js +69 -0
  67. package/dist/session/index.js.map +1 -0
  68. package/dist/session/message.d.ts +26 -0
  69. package/dist/session/message.d.ts.map +1 -0
  70. package/dist/session/message.js +34 -0
  71. package/dist/session/message.js.map +1 -0
  72. package/dist/tools/file.d.ts +46 -0
  73. package/dist/tools/file.d.ts.map +1 -0
  74. package/dist/tools/file.js +100 -0
  75. package/dist/tools/file.js.map +1 -0
  76. package/dist/tools/git.d.ts +52 -0
  77. package/dist/tools/git.d.ts.map +1 -0
  78. package/dist/tools/git.js +112 -0
  79. package/dist/tools/git.js.map +1 -0
  80. package/dist/tools/index.d.ts +42 -0
  81. package/dist/tools/index.d.ts.map +1 -0
  82. package/dist/tools/index.js +92 -0
  83. package/dist/tools/index.js.map +1 -0
  84. package/dist/tools/search.d.ts +36 -0
  85. package/dist/tools/search.d.ts.map +1 -0
  86. package/dist/tools/search.js +223 -0
  87. package/dist/tools/search.js.map +1 -0
  88. package/dist/tools/shell.d.ts +50 -0
  89. package/dist/tools/shell.d.ts.map +1 -0
  90. package/dist/tools/shell.js +117 -0
  91. package/dist/tools/shell.js.map +1 -0
  92. package/dist/tools/types.d.ts +68 -0
  93. package/dist/tools/types.d.ts.map +1 -0
  94. package/dist/tools/types.js +57 -0
  95. package/dist/tools/types.js.map +1 -0
  96. package/dist/tui/index.d.ts +8 -0
  97. package/dist/tui/index.d.ts.map +1 -0
  98. package/dist/tui/index.js +8 -0
  99. package/dist/tui/index.js.map +1 -0
  100. package/dist/tui/input.d.ts +14 -0
  101. package/dist/tui/input.d.ts.map +1 -0
  102. package/dist/tui/input.js +50 -0
  103. package/dist/tui/input.js.map +1 -0
  104. package/dist/tui/output.d.ts +20 -0
  105. package/dist/tui/output.d.ts.map +1 -0
  106. package/dist/tui/output.js +70 -0
  107. package/dist/tui/output.js.map +1 -0
  108. package/dist/tui/repl.d.ts +25 -0
  109. package/dist/tui/repl.d.ts.map +1 -0
  110. package/dist/tui/repl.js +114 -0
  111. package/dist/tui/repl.js.map +1 -0
  112. package/dist/tui/welcome.d.ts +15 -0
  113. package/dist/tui/welcome.d.ts.map +1 -0
  114. package/dist/tui/welcome.js +86 -0
  115. package/dist/tui/welcome.js.map +1 -0
  116. package/dist/types.d.ts +38 -0
  117. package/dist/types.d.ts.map +1 -0
  118. package/dist/types.js +6 -0
  119. package/dist/types.js.map +1 -0
  120. package/dist/utils/diff.d.ts +22 -0
  121. package/dist/utils/diff.d.ts.map +1 -0
  122. package/dist/utils/diff.js +60 -0
  123. package/dist/utils/diff.js.map +1 -0
  124. package/dist/utils/project.d.ts +32 -0
  125. package/dist/utils/project.d.ts.map +1 -0
  126. package/dist/utils/project.js +89 -0
  127. package/dist/utils/project.js.map +1 -0
  128. package/docs/API_KEY_GUIDE.md +236 -0
  129. package/docs/FAQ.md +182 -0
  130. package/install.bat +63 -0
  131. package/install.ps1 +238 -0
  132. package/install.sh +113 -0
  133. package/package.json +36 -0
  134. package/src/app/agent.ts +140 -0
  135. package/src/app/index.ts +101 -0
  136. package/src/cmd/index.ts +96 -0
  137. package/src/config/index.ts +130 -0
  138. package/src/config/providers.ts +160 -0
  139. package/src/config/types.ts +46 -0
  140. package/src/hooks/index.ts +111 -0
  141. package/src/llm/anthropic.ts +146 -0
  142. package/src/llm/gemini.ts +127 -0
  143. package/src/llm/index.ts +209 -0
  144. package/src/llm/openai.ts +132 -0
  145. package/src/llm/provider.ts +38 -0
  146. package/src/mcp/client.ts +330 -0
  147. package/src/prompts/index.ts +123 -0
  148. package/src/session/compactor.ts +103 -0
  149. package/src/session/index.ts +81 -0
  150. package/src/session/message.ts +42 -0
  151. package/src/tools/file.ts +117 -0
  152. package/src/tools/git.ts +132 -0
  153. package/src/tools/index.ts +108 -0
  154. package/src/tools/search.ts +263 -0
  155. package/src/tools/shell.ts +136 -0
  156. package/src/tools/types.ts +122 -0
  157. package/src/tui/index.ts +8 -0
  158. package/src/tui/input.ts +56 -0
  159. package/src/tui/output.ts +83 -0
  160. package/src/tui/repl.ts +131 -0
  161. package/src/tui/welcome.ts +101 -0
  162. package/src/types.ts +42 -0
  163. package/src/utils/diff.ts +71 -0
  164. package/src/utils/project.ts +99 -0
  165. package/tsconfig.json +19 -0
@@ -0,0 +1,160 @@
1
+ /**
2
+ * Provider definitions and model catalogs
3
+ * Consolidated from old types.ts PROVIDERS + welcome.ts model lists
4
+ */
5
+
6
+ import type { ProviderConfig, ProviderName, ModelInfo } from './types.js';
7
+
8
+ // ==================== Provider Definitions ====================
9
+
10
+ export const PROVIDERS: Record<ProviderName, ProviderConfig> = {
11
+ siliconflow: {
12
+ name: 'SiliconFlow (硅基流动)',
13
+ baseUrl: 'https://api.siliconflow.cn/v1',
14
+ defaultModel: 'Qwen/Qwen2.5-7B-Instruct',
15
+ envKeys: ['SILICONFLOW_API_KEY', 'OPENAI_API_KEY'],
16
+ format: 'openai',
17
+ },
18
+ minimax: {
19
+ name: 'MiniMax',
20
+ baseUrl: 'https://api.minimax.io/anthropic/v1',
21
+ defaultModel: 'MiniMax-M2.5',
22
+ envKeys: ['MINIMAX_API_KEY', 'OPENAI_API_KEY'],
23
+ format: 'anthropic',
24
+ },
25
+ openai: {
26
+ name: 'OpenAI',
27
+ baseUrl: 'https://api.openai.com/v1',
28
+ defaultModel: 'gpt-4o-mini',
29
+ envKeys: ['OPENAI_API_KEY'],
30
+ format: 'openai',
31
+ },
32
+ anthropic: {
33
+ name: 'Anthropic',
34
+ baseUrl: 'https://api.anthropic.com/v1',
35
+ defaultModel: 'claude-3-haiku-20240307',
36
+ envKeys: ['ANTHROPIC_API_KEY'],
37
+ format: 'anthropic',
38
+ },
39
+ ollama: {
40
+ name: 'Ollama (Local)',
41
+ baseUrl: process.env.OLLAMA_BASE_URL || 'http://localhost:11434',
42
+ defaultModel: 'llama2',
43
+ envKeys: [],
44
+ format: 'openai',
45
+ },
46
+ gemini: {
47
+ name: 'Google Gemini',
48
+ baseUrl: 'https://generativelanguage.googleapis.com/v1beta',
49
+ defaultModel: 'gemini-1.5-flash',
50
+ envKeys: ['GEMINI_API_KEY'],
51
+ format: 'gemini',
52
+ },
53
+ kimi: {
54
+ name: 'Kimi (Moonshot AI)',
55
+ baseUrl: 'https://api.moonshot.cn/v1',
56
+ defaultModel: 'moonshot-v1-8k',
57
+ envKeys: ['KIMI_API_KEY', 'MOONSHOT_API_KEY'],
58
+ format: 'openai',
59
+ },
60
+ deepseek: {
61
+ name: 'DeepSeek',
62
+ baseUrl: 'https://api.deepseek.com/v1',
63
+ defaultModel: 'deepseek-chat',
64
+ envKeys: ['DEEPSEEK_API_KEY'],
65
+ format: 'openai',
66
+ },
67
+ ernie: {
68
+ name: 'Baidu ERNIE (文心一言)',
69
+ baseUrl: 'https://aip.baidubce.com/rpc/2.0/ai_custom/v1',
70
+ defaultModel: 'ernie-4.0-8k',
71
+ envKeys: ['ERNIE_API_KEY', 'BAIDU_API_KEY'],
72
+ format: 'openai',
73
+ },
74
+ };
75
+
76
+ // ==================== Model Catalogs ====================
77
+
78
+ export const MODEL_CATALOGS: Record<ProviderName, ModelInfo[]> = {
79
+ siliconflow: [
80
+ { id: 'Qwen/Qwen2.5-7B-Instruct', name: 'Qwen2.5-7B (推荐)', desc: '性价比高,适合日常编程' },
81
+ { id: 'Qwen/Qwen2.5-32B-Instruct', name: 'Qwen2.5-32B', desc: '更强性能,适合复杂任务' },
82
+ { id: 'Qwen/Qwen2.5-72B-Instruct', name: 'Qwen2.5-72B', desc: '旗舰模型' },
83
+ { id: 'Pro/moonshotai/Kimi-K2.5', name: 'Kimi-K2.5 (私有)', desc: 'Moonshot 强力模型' },
84
+ { id: 'Pro/deepseek-ai/DeepSeek-V3', name: 'DeepSeek-V3', desc: '深度求索最新模型' },
85
+ { id: 'Pro/deepseek-ai/DeepSeek-R1', name: 'DeepSeek-R1', desc: '推理能力强' },
86
+ { id: 'THUDM/glm-4-9b-chat', name: 'GLM-4-9B', desc: '智谱模型' },
87
+ { id: 'THUDM/glm-4-32b-chat', name: 'GLM-4-32B', desc: '智谱大模型' },
88
+ { id: '01-ai/Yi-1.5-34B-Chat', name: 'Yi-1.5-34B', desc: '零一万物' },
89
+ { id: 'microsoft/WizardLM-2-8x22B', name: 'WizardLM-2', desc: '微软开源' },
90
+ ],
91
+ minimax: [
92
+ { id: 'MiniMax-M2.5', name: 'MiniMax-M2.5 (推荐)', desc: 'Agent 能力最强' },
93
+ { id: 'MiniMax-M2.1', name: 'MiniMax-M2.1', desc: '稳定版本' },
94
+ ],
95
+ openai: [
96
+ { id: 'gpt-4o-mini', name: 'GPT-4o-mini (推荐)', desc: '性价比高' },
97
+ { id: 'gpt-4o', name: 'GPT-4o', desc: '最新旗舰' },
98
+ { id: 'gpt-4-turbo', name: 'GPT-4-Turbo', desc: '强性能' },
99
+ ],
100
+ anthropic: [
101
+ { id: 'claude-3-haiku-20240307', name: 'Claude-3-Haiku (推荐)', desc: '快速响应' },
102
+ { id: 'claude-3.5-sonnet-20241022', name: 'Claude-3.5-Sonnet', desc: '最新旗舰' },
103
+ { id: 'claude-3-opus-20240229', name: 'Claude-3-Opus', desc: '最强性能' },
104
+ ],
105
+ ollama: [
106
+ { id: 'llama2', name: 'Llama 2', desc: 'Meta 开源模型' },
107
+ { id: 'codellama', name: 'Code Llama', desc: '编程专用' },
108
+ { id: 'mistral', name: 'Mistral', desc: '高性能开源' },
109
+ ],
110
+ gemini: [
111
+ { id: 'gemini-1.5-flash-8b', name: 'Gemini-1.5-Flash-8B (推荐)', desc: '免费快速' },
112
+ { id: 'gemini-1.5-flash', name: 'Gemini-1.5-Flash', desc: '性价比高' },
113
+ { id: 'gemini-1.5-pro', name: 'Gemini-1.5-Pro', desc: '最新旗舰' },
114
+ ],
115
+ kimi: [
116
+ { id: 'moonshot-v1-8k', name: 'Moonshot-V1-8K (推荐)', desc: '日常编程' },
117
+ { id: 'moonshot-v1-32k', name: 'Moonshot-V1-32K', desc: '更长上下文' },
118
+ { id: 'moonshot-v1-128k', name: 'Moonshot-V1-128K', desc: '超长上下文' },
119
+ ],
120
+ deepseek: [
121
+ { id: 'deepseek-chat', name: 'DeepSeek-Chat (推荐)', desc: '性价比高' },
122
+ { id: 'deepseek-coder', name: 'DeepSeek-Coder', desc: '编程专用' },
123
+ ],
124
+ ernie: [
125
+ { id: 'ernie-4.0-8k', name: 'ERNIE-4.0-8K (推荐)', desc: '百度旗舰' },
126
+ { id: 'ernie-3.5-8k', name: 'ERNIE-3.5-8K', desc: '性价比高' },
127
+ ],
128
+ };
129
+
130
+ // ==================== Helpers ====================
131
+
132
+ /**
133
+ * Get models for a specific provider
134
+ */
135
+ export function getModelsForProvider(provider: ProviderName): ModelInfo[] {
136
+ return MODEL_CATALOGS[provider] || MODEL_CATALOGS.siliconflow;
137
+ }
138
+
139
+ /**
140
+ * Get API key for a provider from environment variables
141
+ */
142
+ export function getApiKeyFromEnv(provider: ProviderName): string {
143
+ const providerConfig = PROVIDERS[provider];
144
+ if (!providerConfig) return '';
145
+
146
+ for (const envKey of providerConfig.envKeys) {
147
+ if (process.env[envKey]) return process.env[envKey];
148
+ }
149
+ return '';
150
+ }
151
+
152
+ /**
153
+ * List all provider names
154
+ */
155
+ export function listProviders(): Array<{ key: ProviderName; name: string }> {
156
+ return Object.entries(PROVIDERS).map(([key, val]) => ({
157
+ key: key as ProviderName,
158
+ name: val.name,
159
+ }));
160
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Configuration types for Thatgfsj Code
3
+ */
4
+
5
+ export type ProviderName =
6
+ | 'siliconflow'
7
+ | 'minimax'
8
+ | 'openai'
9
+ | 'anthropic'
10
+ | 'ollama'
11
+ | 'gemini'
12
+ | 'kimi'
13
+ | 'deepseek'
14
+ | 'ernie';
15
+
16
+ export interface ProviderConfig {
17
+ name: string;
18
+ baseUrl: string;
19
+ defaultModel: string;
20
+ envKeys: string[];
21
+ format: 'openai' | 'anthropic' | 'gemini';
22
+ }
23
+
24
+ export interface Config {
25
+ model: string;
26
+ apiKey: string;
27
+ temperature: number;
28
+ maxTokens: number;
29
+ provider: ProviderName;
30
+ baseUrl?: string;
31
+ }
32
+
33
+ export interface AIConfig {
34
+ model: string;
35
+ apiKey?: string;
36
+ temperature?: number;
37
+ maxTokens?: number;
38
+ baseUrl?: string;
39
+ provider?: ProviderName;
40
+ }
41
+
42
+ export interface ModelInfo {
43
+ id: string;
44
+ name: string;
45
+ desc: string;
46
+ }
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Hooks System - Event-driven lifecycle hooks
3
+ * Migrated from old src/core/hooks.ts
4
+ */
5
+
6
+ import type { ChatMessage, ToolCall } from '../types.js';
7
+
8
+ // ==================== Hook Types ====================
9
+
10
+ export type HookName =
11
+ | 'beforeToolCall'
12
+ | 'afterToolCall'
13
+ | 'beforeAgentLoop'
14
+ | 'afterAgentLoop'
15
+ | 'onError';
16
+
17
+ export interface HookContext {
18
+ toolName?: string;
19
+ toolParams?: Record<string, any>;
20
+ toolResult?: any;
21
+ messages?: ChatMessage[];
22
+ error?: Error;
23
+ iteration?: number;
24
+ [key: string]: any;
25
+ }
26
+
27
+ export type HookFn = (ctx: HookContext) => void | Promise<void>;
28
+
29
+ interface HookRegistration {
30
+ name: HookName;
31
+ fn: HookFn;
32
+ priority?: number;
33
+ once?: boolean;
34
+ }
35
+
36
+ // ==================== Hook Manager ====================
37
+
38
+ export class HookManager {
39
+ private hooks: Map<HookName, HookRegistration[]> = new Map();
40
+
41
+ constructor() {
42
+ const allHooks: HookName[] = ['beforeToolCall', 'afterToolCall', 'beforeAgentLoop', 'afterAgentLoop', 'onError'];
43
+ for (const h of allHooks) {
44
+ this.hooks.set(h, []);
45
+ }
46
+ }
47
+
48
+ register(name: HookName, fn: HookFn, priority = 0, once = false): void {
49
+ const regs = this.hooks.get(name) || [];
50
+ regs.push({ name, fn, priority, once });
51
+ regs.sort((a, b) => (b.priority || 0) - (a.priority || 0));
52
+ this.hooks.set(name, regs);
53
+ }
54
+
55
+ unregister(name: HookName, fn: HookFn): void {
56
+ const regs = this.hooks.get(name) || [];
57
+ this.hooks.set(name, regs.filter(r => r.fn !== fn));
58
+ }
59
+
60
+ clear(name?: HookName): void {
61
+ if (name) {
62
+ this.hooks.set(name, []);
63
+ } else {
64
+ for (const regs of this.hooks.values()) {
65
+ regs.length = 0;
66
+ }
67
+ }
68
+ }
69
+
70
+ async emit(name: HookName, ctx: HookContext): Promise<void> {
71
+ const regs = this.hooks.get(name) || [];
72
+ for (const reg of regs) {
73
+ try {
74
+ await Promise.resolve(reg.fn(ctx));
75
+ } catch (error: any) {
76
+ console.error(`[Hook ${name}] error:`, error.message);
77
+ }
78
+ if (reg.once) {
79
+ this.unregister(name, reg.fn);
80
+ }
81
+ }
82
+ }
83
+
84
+ hasHooks(name: HookName): boolean {
85
+ return (this.hooks.get(name)?.length || 0) > 0;
86
+ }
87
+ }
88
+
89
+ // ==================== Built-in Hooks ====================
90
+
91
+ /**
92
+ * Audit logging hook - logs all tool calls
93
+ */
94
+ export function auditLogHook(ctx: HookContext): void {
95
+ if (ctx.toolName) {
96
+ const ts = new Date().toISOString().split('T')[1].split('.')[0];
97
+ const status = ctx.toolResult?.success ? 'OK' : 'FAIL';
98
+ console.log(`[AUDIT ${ts}] ${status} | tool=${ctx.toolName} | params=${JSON.stringify(ctx.toolParams || {})}`);
99
+ }
100
+ }
101
+
102
+ // ==================== Singleton ====================
103
+
104
+ let globalHookManager: HookManager | null = null;
105
+
106
+ export function getHookManager(): HookManager {
107
+ if (!globalHookManager) {
108
+ globalHookManager = new HookManager();
109
+ }
110
+ return globalHookManager;
111
+ }
@@ -0,0 +1,146 @@
1
+ /**
2
+ * Anthropic Provider
3
+ * Works with: Anthropic Claude, MiniMax (Anthropic mode)
4
+ */
5
+
6
+ import type { ChatMessage, ChatResponse, ChatOptions } from '../types.js';
7
+ import type { Tool } from '../tools/types.js';
8
+ import type { LLMProvider, ProviderConfig } from './provider.js';
9
+
10
+ export class AnthropicProvider implements LLMProvider {
11
+ readonly name = 'anthropic';
12
+ private config: ProviderConfig;
13
+
14
+ constructor(config: ProviderConfig) {
15
+ this.config = config;
16
+ }
17
+
18
+ buildTools(tools: Tool[]): any[] {
19
+ return tools.map(tool => ({
20
+ name: tool.name,
21
+ description: tool.description,
22
+ input_schema: tool.inputSchema || {
23
+ type: 'object',
24
+ properties: Object.fromEntries(
25
+ tool.parameters.map(p => [p.name, { type: p.type, description: p.description }])
26
+ ),
27
+ required: tool.parameters.filter(p => p.required).map(p => p.name),
28
+ },
29
+ }));
30
+ }
31
+
32
+ async chat(messages: ChatMessage[], options?: ChatOptions): Promise<ChatResponse> {
33
+ const body = this.buildRequest(messages, false, options);
34
+
35
+ const response = await fetch(`${this.config.baseUrl}/messages`, {
36
+ method: 'POST',
37
+ headers: {
38
+ 'Content-Type': 'application/json',
39
+ 'x-api-key': this.config.apiKey,
40
+ 'anthropic-version': '2023-06-01',
41
+ },
42
+ body: JSON.stringify(body),
43
+ });
44
+
45
+ if (!response.ok) {
46
+ const text = await response.text().catch(() => '');
47
+ throw new Error(`Anthropic API error ${response.status}: ${text}`);
48
+ }
49
+
50
+ const data = await response.json();
51
+ const contentBlock = data.content?.find((b: any) => b.type === 'text');
52
+ const toolUseBlocks = data.content?.filter((b: any) => b.type === 'tool_use') || [];
53
+
54
+ return {
55
+ content: contentBlock?.text || '',
56
+ role: 'assistant',
57
+ usage: data.usage ? {
58
+ prompt_tokens: data.usage.input_tokens || 0,
59
+ completion_tokens: data.usage.output_tokens || 0,
60
+ total_tokens: (data.usage.input_tokens || 0) + (data.usage.output_tokens || 0),
61
+ } : undefined,
62
+ tool_calls: toolUseBlocks.length > 0 ? toolUseBlocks.map((b: any) => ({
63
+ id: b.id,
64
+ type: 'function' as const,
65
+ function: {
66
+ name: b.name,
67
+ arguments: JSON.stringify(b.input),
68
+ },
69
+ })) : undefined,
70
+ };
71
+ }
72
+
73
+ async *chatStream(messages: ChatMessage[], options?: ChatOptions): AsyncGenerator<string, ChatResponse> {
74
+ const body = this.buildRequest(messages, true, options);
75
+
76
+ const response = await fetch(`${this.config.baseUrl}/messages`, {
77
+ method: 'POST',
78
+ headers: {
79
+ 'Content-Type': 'application/json',
80
+ 'x-api-key': this.config.apiKey,
81
+ 'anthropic-version': '2023-06-01',
82
+ 'anthropic-dangerous-direct-browser-access': 'true',
83
+ },
84
+ body: JSON.stringify(body),
85
+ });
86
+
87
+ if (!response.ok || !response.body) {
88
+ const text = await response.text().catch(() => '');
89
+ throw new Error(`Anthropic API error ${response.status}: ${text}`);
90
+ }
91
+
92
+ const reader = response.body.getReader();
93
+ const decoder = new TextDecoder();
94
+ let fullContent = '';
95
+ let buffer = '';
96
+
97
+ try {
98
+ while (true) {
99
+ const { done, value } = await reader.read();
100
+ if (done) break;
101
+
102
+ buffer += decoder.decode(value, { stream: true });
103
+ const lines = buffer.split('\n');
104
+ buffer = lines.pop() || '';
105
+
106
+ for (const line of lines) {
107
+ const trimmed = line.trim();
108
+ if (!trimmed || !trimmed.startsWith('data: ')) continue;
109
+
110
+ try {
111
+ const data = JSON.parse(trimmed.slice(6));
112
+ if (data.type === 'content_block_delta' && data.delta?.type === 'text_delta') {
113
+ const text = data.delta.text;
114
+ fullContent += text;
115
+ yield text;
116
+ }
117
+ } catch {
118
+ // Skip invalid JSON
119
+ }
120
+ }
121
+ }
122
+ } finally {
123
+ reader.releaseLock();
124
+ }
125
+
126
+ return { content: fullContent, role: 'assistant' };
127
+ }
128
+
129
+ private buildRequest(messages: ChatMessage[], stream: boolean, options?: ChatOptions) {
130
+ // Anthropic uses separate system message
131
+ const systemMsg = messages.find(m => m.role === 'system');
132
+ const nonSystemMsgs = messages.filter(m => m.role !== 'system');
133
+
134
+ return {
135
+ model: this.config.model,
136
+ max_tokens: options?.maxTokens ?? this.config.maxTokens,
137
+ temperature: options?.temperature ?? this.config.temperature,
138
+ ...(systemMsg && { system: systemMsg.content }),
139
+ messages: nonSystemMsgs.map(m => ({
140
+ role: m.role === 'assistant' ? 'assistant' : 'user',
141
+ content: m.content,
142
+ })),
143
+ stream,
144
+ };
145
+ }
146
+ }
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Google Gemini Provider
3
+ */
4
+
5
+ import type { ChatMessage, ChatResponse, ChatOptions } from '../types.js';
6
+ import type { Tool } from '../tools/types.js';
7
+ import type { LLMProvider, ProviderConfig } from './provider.js';
8
+
9
+ export class GeminiProvider implements LLMProvider {
10
+ readonly name = 'gemini';
11
+ private config: ProviderConfig;
12
+
13
+ constructor(config: ProviderConfig) {
14
+ this.config = config;
15
+ }
16
+
17
+ buildTools(tools: Tool[]): any[] {
18
+ return tools.map(tool => ({
19
+ name: tool.name,
20
+ description: tool.description,
21
+ parameters: tool.inputSchema || {
22
+ type: 'object',
23
+ properties: Object.fromEntries(
24
+ tool.parameters.map(p => [p.name, { type: p.type, description: p.description }])
25
+ ),
26
+ required: tool.parameters.filter(p => p.required).map(p => p.name),
27
+ },
28
+ }));
29
+ }
30
+
31
+ async chat(messages: ChatMessage[], options?: ChatOptions): Promise<ChatResponse> {
32
+ const body = this.buildRequest(messages, options);
33
+
34
+ const url = `${this.config.baseUrl}/models/${this.config.model}:generateContent?key=${this.config.apiKey}`;
35
+ const response = await fetch(url, {
36
+ method: 'POST',
37
+ headers: { 'Content-Type': 'application/json' },
38
+ body: JSON.stringify(body),
39
+ });
40
+
41
+ if (!response.ok) {
42
+ const text = await response.text().catch(() => '');
43
+ throw new Error(`Gemini API error ${response.status}: ${text}`);
44
+ }
45
+
46
+ const data = await response.json();
47
+ const candidate = data.candidates?.[0];
48
+ const text = candidate?.content?.parts?.map((p: any) => p.text).join('') || '';
49
+
50
+ return {
51
+ content: text,
52
+ role: 'assistant',
53
+ usage: data.usageMetadata ? {
54
+ prompt_tokens: data.usageMetadata.promptTokenCount || 0,
55
+ completion_tokens: data.usageMetadata.candidatesTokenCount || 0,
56
+ total_tokens: data.usageMetadata.totalTokenCount || 0,
57
+ } : undefined,
58
+ };
59
+ }
60
+
61
+ async *chatStream(messages: ChatMessage[], options?: ChatOptions): AsyncGenerator<string, ChatResponse> {
62
+ const body = this.buildRequest(messages, options);
63
+
64
+ const url = `${this.config.baseUrl}/models/${this.config.model}:streamGenerateContent?key=${this.config.apiKey}`;
65
+ const response = await fetch(url, {
66
+ method: 'POST',
67
+ headers: { 'Content-Type': 'application/json' },
68
+ body: JSON.stringify(body),
69
+ });
70
+
71
+ if (!response.ok || !response.body) {
72
+ const text = await response.text().catch(() => '');
73
+ throw new Error(`Gemini API error ${response.status}: ${text}`);
74
+ }
75
+
76
+ const reader = response.body.getReader();
77
+ const decoder = new TextDecoder();
78
+ let fullContent = '';
79
+ let buffer = '';
80
+
81
+ try {
82
+ while (true) {
83
+ const { done, value } = await reader.read();
84
+ if (done) break;
85
+
86
+ buffer += decoder.decode(value, { stream: true });
87
+
88
+ // Gemini streams JSON array chunks
89
+ const jsonMatch = buffer.match(/\{"candidates":\[.*?\]\}/);
90
+ if (jsonMatch) {
91
+ try {
92
+ const data = JSON.parse(jsonMatch[0]);
93
+ const text = data.candidates?.[0]?.content?.parts?.map((p: any) => p.text).join('') || '';
94
+ if (text) {
95
+ fullContent += text;
96
+ yield text;
97
+ }
98
+ } catch {
99
+ // Skip invalid JSON
100
+ }
101
+ buffer = buffer.slice(jsonMatch.index! + jsonMatch[0].length);
102
+ }
103
+ }
104
+ } finally {
105
+ reader.releaseLock();
106
+ }
107
+
108
+ return { content: fullContent, role: 'assistant' };
109
+ }
110
+
111
+ private buildRequest(messages: ChatMessage[], options?: ChatOptions) {
112
+ const contents = messages
113
+ .filter(m => m.role !== 'system')
114
+ .map(m => ({
115
+ role: m.role === 'assistant' ? 'model' : 'user',
116
+ parts: [{ text: m.content }],
117
+ }));
118
+
119
+ return {
120
+ contents,
121
+ generationConfig: {
122
+ temperature: options?.temperature ?? this.config.temperature,
123
+ maxOutputTokens: options?.maxTokens ?? this.config.maxTokens,
124
+ },
125
+ };
126
+ }
127
+ }