@build-astron-co/nimbus 0.2.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 (313) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +628 -0
  3. package/bin/nimbus +38 -0
  4. package/package.json +80 -0
  5. package/src/__tests__/app.test.ts +76 -0
  6. package/src/__tests__/audit.test.ts +877 -0
  7. package/src/__tests__/circuit-breaker.test.ts +116 -0
  8. package/src/__tests__/cli-run.test.ts +115 -0
  9. package/src/__tests__/context-manager.test.ts +502 -0
  10. package/src/__tests__/context.test.ts +242 -0
  11. package/src/__tests__/enterprise.test.ts +401 -0
  12. package/src/__tests__/generator.test.ts +433 -0
  13. package/src/__tests__/hooks.test.ts +582 -0
  14. package/src/__tests__/init.test.ts +436 -0
  15. package/src/__tests__/intent-parser.test.ts +229 -0
  16. package/src/__tests__/llm-router.test.ts +209 -0
  17. package/src/__tests__/lsp.test.ts +293 -0
  18. package/src/__tests__/modes.test.ts +336 -0
  19. package/src/__tests__/permissions.test.ts +338 -0
  20. package/src/__tests__/serve.test.ts +275 -0
  21. package/src/__tests__/sessions.test.ts +227 -0
  22. package/src/__tests__/sharing.test.ts +288 -0
  23. package/src/__tests__/snapshots.test.ts +581 -0
  24. package/src/__tests__/state-db.test.ts +334 -0
  25. package/src/__tests__/stream-with-tools.test.ts +732 -0
  26. package/src/__tests__/subagents.test.ts +176 -0
  27. package/src/__tests__/system-prompt.test.ts +169 -0
  28. package/src/__tests__/tool-converter.test.ts +256 -0
  29. package/src/__tests__/tool-schemas.test.ts +397 -0
  30. package/src/__tests__/tools.test.ts +143 -0
  31. package/src/__tests__/version.test.ts +49 -0
  32. package/src/agent/compaction-agent.ts +227 -0
  33. package/src/agent/context-manager.ts +435 -0
  34. package/src/agent/context.ts +427 -0
  35. package/src/agent/deploy-preview.ts +426 -0
  36. package/src/agent/index.ts +68 -0
  37. package/src/agent/loop.ts +717 -0
  38. package/src/agent/modes.ts +429 -0
  39. package/src/agent/permissions.ts +466 -0
  40. package/src/agent/subagents/base.ts +116 -0
  41. package/src/agent/subagents/cost.ts +51 -0
  42. package/src/agent/subagents/explore.ts +42 -0
  43. package/src/agent/subagents/general.ts +54 -0
  44. package/src/agent/subagents/index.ts +102 -0
  45. package/src/agent/subagents/infra.ts +59 -0
  46. package/src/agent/subagents/security.ts +69 -0
  47. package/src/agent/system-prompt.ts +436 -0
  48. package/src/app.ts +122 -0
  49. package/src/audit/activity-log.ts +290 -0
  50. package/src/audit/compliance-checker.ts +540 -0
  51. package/src/audit/cost-tracker.ts +318 -0
  52. package/src/audit/index.ts +23 -0
  53. package/src/audit/security-scanner.ts +596 -0
  54. package/src/auth/guard.ts +75 -0
  55. package/src/auth/index.ts +56 -0
  56. package/src/auth/oauth.ts +455 -0
  57. package/src/auth/providers.ts +470 -0
  58. package/src/auth/sso.ts +113 -0
  59. package/src/auth/store.ts +505 -0
  60. package/src/auth/types.ts +187 -0
  61. package/src/build.ts +141 -0
  62. package/src/cli/index.ts +16 -0
  63. package/src/cli/init.ts +854 -0
  64. package/src/cli/openapi-spec.ts +356 -0
  65. package/src/cli/run.ts +237 -0
  66. package/src/cli/serve-auth.ts +80 -0
  67. package/src/cli/serve.ts +462 -0
  68. package/src/cli/web.ts +67 -0
  69. package/src/cli.ts +1417 -0
  70. package/src/clients/core-engine-client.ts +227 -0
  71. package/src/clients/enterprise-client.ts +334 -0
  72. package/src/clients/generator-client.ts +351 -0
  73. package/src/clients/git-client.ts +627 -0
  74. package/src/clients/github-client.ts +410 -0
  75. package/src/clients/helm-client.ts +504 -0
  76. package/src/clients/index.ts +80 -0
  77. package/src/clients/k8s-client.ts +497 -0
  78. package/src/clients/llm-client.ts +161 -0
  79. package/src/clients/rest-client.ts +130 -0
  80. package/src/clients/service-discovery.ts +33 -0
  81. package/src/clients/terraform-client.ts +482 -0
  82. package/src/clients/tools-client.ts +1843 -0
  83. package/src/clients/ws-client.ts +115 -0
  84. package/src/commands/analyze/index.ts +352 -0
  85. package/src/commands/apply/helm.ts +473 -0
  86. package/src/commands/apply/index.ts +213 -0
  87. package/src/commands/apply/k8s.ts +454 -0
  88. package/src/commands/apply/terraform.ts +582 -0
  89. package/src/commands/ask.ts +167 -0
  90. package/src/commands/audit/index.ts +238 -0
  91. package/src/commands/auth-cloud.ts +294 -0
  92. package/src/commands/auth-list.ts +134 -0
  93. package/src/commands/auth-profile.ts +121 -0
  94. package/src/commands/auth-status.ts +141 -0
  95. package/src/commands/aws/ec2.ts +501 -0
  96. package/src/commands/aws/iam.ts +397 -0
  97. package/src/commands/aws/index.ts +133 -0
  98. package/src/commands/aws/lambda.ts +396 -0
  99. package/src/commands/aws/rds.ts +439 -0
  100. package/src/commands/aws/s3.ts +439 -0
  101. package/src/commands/aws/vpc.ts +393 -0
  102. package/src/commands/aws-discover.ts +649 -0
  103. package/src/commands/aws-terraform.ts +805 -0
  104. package/src/commands/azure/aks.ts +376 -0
  105. package/src/commands/azure/functions.ts +253 -0
  106. package/src/commands/azure/index.ts +116 -0
  107. package/src/commands/azure/storage.ts +478 -0
  108. package/src/commands/azure/vm.ts +355 -0
  109. package/src/commands/billing/index.ts +256 -0
  110. package/src/commands/chat.ts +314 -0
  111. package/src/commands/config.ts +346 -0
  112. package/src/commands/cost/cloud-cost-estimator.ts +266 -0
  113. package/src/commands/cost/estimator.ts +79 -0
  114. package/src/commands/cost/index.ts +594 -0
  115. package/src/commands/cost/parsers/terraform.ts +273 -0
  116. package/src/commands/cost/parsers/types.ts +25 -0
  117. package/src/commands/cost/pricing/aws.ts +544 -0
  118. package/src/commands/cost/pricing/azure.ts +499 -0
  119. package/src/commands/cost/pricing/gcp.ts +396 -0
  120. package/src/commands/cost/pricing/index.ts +40 -0
  121. package/src/commands/demo.ts +250 -0
  122. package/src/commands/doctor.ts +794 -0
  123. package/src/commands/drift/index.ts +439 -0
  124. package/src/commands/explain.ts +277 -0
  125. package/src/commands/feedback.ts +389 -0
  126. package/src/commands/fix.ts +324 -0
  127. package/src/commands/fs/index.ts +402 -0
  128. package/src/commands/gcp/compute.ts +325 -0
  129. package/src/commands/gcp/functions.ts +271 -0
  130. package/src/commands/gcp/gke.ts +438 -0
  131. package/src/commands/gcp/iam.ts +344 -0
  132. package/src/commands/gcp/index.ts +129 -0
  133. package/src/commands/gcp/storage.ts +284 -0
  134. package/src/commands/generate-helm.ts +1249 -0
  135. package/src/commands/generate-k8s.ts +1560 -0
  136. package/src/commands/generate-terraform.ts +1460 -0
  137. package/src/commands/gh/index.ts +863 -0
  138. package/src/commands/git/index.ts +1343 -0
  139. package/src/commands/helm/index.ts +1126 -0
  140. package/src/commands/help.ts +539 -0
  141. package/src/commands/history.ts +142 -0
  142. package/src/commands/import.ts +868 -0
  143. package/src/commands/index.ts +367 -0
  144. package/src/commands/init.ts +1046 -0
  145. package/src/commands/k8s/index.ts +1137 -0
  146. package/src/commands/login.ts +631 -0
  147. package/src/commands/logout.ts +83 -0
  148. package/src/commands/onboarding.ts +228 -0
  149. package/src/commands/plan/display.ts +279 -0
  150. package/src/commands/plan/index.ts +599 -0
  151. package/src/commands/preview.ts +452 -0
  152. package/src/commands/questionnaire.ts +1270 -0
  153. package/src/commands/resume.ts +55 -0
  154. package/src/commands/team/index.ts +346 -0
  155. package/src/commands/template.ts +232 -0
  156. package/src/commands/tf/index.ts +1034 -0
  157. package/src/commands/upgrade.ts +550 -0
  158. package/src/commands/usage/index.ts +134 -0
  159. package/src/commands/version.ts +170 -0
  160. package/src/compat/index.ts +2 -0
  161. package/src/compat/runtime.ts +12 -0
  162. package/src/compat/sqlite.ts +107 -0
  163. package/src/config/index.ts +17 -0
  164. package/src/config/manager.ts +530 -0
  165. package/src/config/safety-policy.ts +358 -0
  166. package/src/config/schema.ts +125 -0
  167. package/src/config/types.ts +527 -0
  168. package/src/context/context-db.ts +199 -0
  169. package/src/demo/index.ts +349 -0
  170. package/src/demo/scenarios/full-journey.ts +229 -0
  171. package/src/demo/scenarios/getting-started.ts +127 -0
  172. package/src/demo/scenarios/helm-release.ts +341 -0
  173. package/src/demo/scenarios/k8s-deployment.ts +194 -0
  174. package/src/demo/scenarios/terraform-vpc.ts +170 -0
  175. package/src/demo/types.ts +92 -0
  176. package/src/engine/cost-estimator.ts +438 -0
  177. package/src/engine/diagram-generator.ts +256 -0
  178. package/src/engine/drift-detector.ts +902 -0
  179. package/src/engine/executor.ts +1035 -0
  180. package/src/engine/index.ts +76 -0
  181. package/src/engine/orchestrator.ts +636 -0
  182. package/src/engine/planner.ts +720 -0
  183. package/src/engine/safety.ts +743 -0
  184. package/src/engine/verifier.ts +770 -0
  185. package/src/enterprise/audit.ts +348 -0
  186. package/src/enterprise/auth.ts +270 -0
  187. package/src/enterprise/billing.ts +822 -0
  188. package/src/enterprise/index.ts +17 -0
  189. package/src/enterprise/teams.ts +443 -0
  190. package/src/generator/best-practices.ts +1608 -0
  191. package/src/generator/helm.ts +630 -0
  192. package/src/generator/index.ts +37 -0
  193. package/src/generator/intent-parser.ts +514 -0
  194. package/src/generator/kubernetes.ts +976 -0
  195. package/src/generator/terraform.ts +1867 -0
  196. package/src/history/index.ts +8 -0
  197. package/src/history/manager.ts +322 -0
  198. package/src/history/types.ts +34 -0
  199. package/src/hooks/config.ts +432 -0
  200. package/src/hooks/engine.ts +391 -0
  201. package/src/hooks/index.ts +4 -0
  202. package/src/llm/auth-bridge.ts +198 -0
  203. package/src/llm/circuit-breaker.ts +140 -0
  204. package/src/llm/config-loader.ts +201 -0
  205. package/src/llm/cost-calculator.ts +171 -0
  206. package/src/llm/index.ts +8 -0
  207. package/src/llm/model-aliases.ts +115 -0
  208. package/src/llm/provider-registry.ts +63 -0
  209. package/src/llm/providers/anthropic.ts +433 -0
  210. package/src/llm/providers/bedrock.ts +477 -0
  211. package/src/llm/providers/google.ts +405 -0
  212. package/src/llm/providers/ollama.ts +767 -0
  213. package/src/llm/providers/openai-compatible.ts +340 -0
  214. package/src/llm/providers/openai.ts +328 -0
  215. package/src/llm/providers/openrouter.ts +338 -0
  216. package/src/llm/router.ts +1035 -0
  217. package/src/llm/types.ts +232 -0
  218. package/src/lsp/client.ts +298 -0
  219. package/src/lsp/languages.ts +116 -0
  220. package/src/lsp/manager.ts +278 -0
  221. package/src/mcp/client.ts +402 -0
  222. package/src/mcp/index.ts +5 -0
  223. package/src/mcp/manager.ts +133 -0
  224. package/src/nimbus.ts +214 -0
  225. package/src/plugins/index.ts +27 -0
  226. package/src/plugins/loader.ts +334 -0
  227. package/src/plugins/manager.ts +376 -0
  228. package/src/plugins/types.ts +284 -0
  229. package/src/scanners/cicd-scanner.ts +258 -0
  230. package/src/scanners/cloud-scanner.ts +466 -0
  231. package/src/scanners/framework-scanner.ts +469 -0
  232. package/src/scanners/iac-scanner.ts +388 -0
  233. package/src/scanners/index.ts +539 -0
  234. package/src/scanners/language-scanner.ts +276 -0
  235. package/src/scanners/package-manager-scanner.ts +277 -0
  236. package/src/scanners/types.ts +172 -0
  237. package/src/sessions/manager.ts +365 -0
  238. package/src/sessions/types.ts +44 -0
  239. package/src/sharing/sync.ts +296 -0
  240. package/src/sharing/viewer.ts +97 -0
  241. package/src/snapshots/index.ts +2 -0
  242. package/src/snapshots/manager.ts +530 -0
  243. package/src/state/artifacts.ts +147 -0
  244. package/src/state/audit.ts +137 -0
  245. package/src/state/billing.ts +240 -0
  246. package/src/state/checkpoints.ts +117 -0
  247. package/src/state/config.ts +67 -0
  248. package/src/state/conversations.ts +14 -0
  249. package/src/state/credentials.ts +154 -0
  250. package/src/state/db.ts +58 -0
  251. package/src/state/index.ts +26 -0
  252. package/src/state/messages.ts +115 -0
  253. package/src/state/projects.ts +123 -0
  254. package/src/state/schema.ts +236 -0
  255. package/src/state/sessions.ts +147 -0
  256. package/src/state/teams.ts +200 -0
  257. package/src/telemetry.ts +108 -0
  258. package/src/tools/aws-ops.ts +952 -0
  259. package/src/tools/azure-ops.ts +579 -0
  260. package/src/tools/file-ops.ts +593 -0
  261. package/src/tools/gcp-ops.ts +625 -0
  262. package/src/tools/git-ops.ts +773 -0
  263. package/src/tools/github-ops.ts +799 -0
  264. package/src/tools/helm-ops.ts +943 -0
  265. package/src/tools/index.ts +17 -0
  266. package/src/tools/k8s-ops.ts +819 -0
  267. package/src/tools/schemas/converter.ts +184 -0
  268. package/src/tools/schemas/devops.ts +612 -0
  269. package/src/tools/schemas/index.ts +73 -0
  270. package/src/tools/schemas/standard.ts +1144 -0
  271. package/src/tools/schemas/types.ts +705 -0
  272. package/src/tools/terraform-ops.ts +862 -0
  273. package/src/types/ambient.d.ts +193 -0
  274. package/src/types/config.ts +83 -0
  275. package/src/types/drift.ts +116 -0
  276. package/src/types/enterprise.ts +335 -0
  277. package/src/types/index.ts +20 -0
  278. package/src/types/plan.ts +44 -0
  279. package/src/types/request.ts +65 -0
  280. package/src/types/response.ts +54 -0
  281. package/src/types/service.ts +51 -0
  282. package/src/ui/App.tsx +997 -0
  283. package/src/ui/DeployPreview.tsx +169 -0
  284. package/src/ui/Header.tsx +68 -0
  285. package/src/ui/InputBox.tsx +350 -0
  286. package/src/ui/MessageList.tsx +585 -0
  287. package/src/ui/PermissionPrompt.tsx +151 -0
  288. package/src/ui/StatusBar.tsx +158 -0
  289. package/src/ui/ToolCallDisplay.tsx +409 -0
  290. package/src/ui/chat-ui.ts +853 -0
  291. package/src/ui/index.ts +33 -0
  292. package/src/ui/ink/index.ts +711 -0
  293. package/src/ui/streaming.ts +176 -0
  294. package/src/ui/types.ts +57 -0
  295. package/src/utils/analytics.ts +72 -0
  296. package/src/utils/cost-warning.ts +27 -0
  297. package/src/utils/env.ts +46 -0
  298. package/src/utils/errors.ts +69 -0
  299. package/src/utils/event-bus.ts +38 -0
  300. package/src/utils/index.ts +24 -0
  301. package/src/utils/logger.ts +171 -0
  302. package/src/utils/rate-limiter.ts +121 -0
  303. package/src/utils/service-auth.ts +49 -0
  304. package/src/utils/validation.ts +53 -0
  305. package/src/version.ts +4 -0
  306. package/src/watcher/index.ts +163 -0
  307. package/src/wizard/approval.ts +383 -0
  308. package/src/wizard/index.ts +25 -0
  309. package/src/wizard/prompts.ts +338 -0
  310. package/src/wizard/types.ts +171 -0
  311. package/src/wizard/ui.ts +556 -0
  312. package/src/wizard/wizard.ts +304 -0
  313. package/tsconfig.json +24 -0
@@ -0,0 +1,340 @@
1
+ /**
2
+ * Generic OpenAI-Compatible Provider
3
+ * Works with any provider that implements the OpenAI chat completions API:
4
+ * Groq, Together AI, DeepSeek, Fireworks AI, Perplexity, Mistral AI, etc.
5
+ */
6
+
7
+ import OpenAI from 'openai';
8
+ import {
9
+ BaseProvider,
10
+ getTextContent,
11
+ type CompletionRequest,
12
+ type LLMMessage,
13
+ type LLMResponse,
14
+ type StreamChunk,
15
+ type ToolCall,
16
+ type ToolCompletionRequest,
17
+ } from '../types';
18
+
19
+ export interface OpenAICompatibleConfig {
20
+ name: string;
21
+ apiKey: string;
22
+ baseURL: string;
23
+ defaultModel: string;
24
+ defaultHeaders?: Record<string, string>;
25
+ }
26
+
27
+ export class OpenAICompatibleProvider extends BaseProvider {
28
+ name: string;
29
+ private client: OpenAI;
30
+ private defaultModel: string;
31
+
32
+ constructor(config: OpenAICompatibleConfig) {
33
+ super();
34
+ this.name = config.name;
35
+ this.defaultModel = config.defaultModel;
36
+ this.client = new OpenAI({
37
+ apiKey: config.apiKey,
38
+ baseURL: config.baseURL,
39
+ defaultHeaders: config.defaultHeaders,
40
+ });
41
+ }
42
+
43
+ async complete(request: CompletionRequest): Promise<LLMResponse> {
44
+ const messages = this.convertMessages(request.messages);
45
+
46
+ const response = await this.client.chat.completions.create({
47
+ model: request.model || this.defaultModel,
48
+ messages,
49
+ max_tokens: request.maxTokens,
50
+ temperature: request.temperature,
51
+ stop: request.stopSequences,
52
+ response_format: request.responseFormat,
53
+ });
54
+
55
+ const choice = response.choices?.[0];
56
+ if (!choice) {
57
+ throw new Error(`${this.name} response missing choices`);
58
+ }
59
+
60
+ return {
61
+ content: choice.message.content || '',
62
+ toolCalls: choice.message.tool_calls?.map(tc => this.convertToolCall(tc)),
63
+ usage: {
64
+ promptTokens: response.usage?.prompt_tokens || 0,
65
+ completionTokens: response.usage?.completion_tokens || 0,
66
+ totalTokens: response.usage?.total_tokens || 0,
67
+ },
68
+ model: response.model,
69
+ finishReason: this.mapFinishReason(choice.finish_reason),
70
+ };
71
+ }
72
+
73
+ async *stream(request: CompletionRequest): AsyncIterable<StreamChunk> {
74
+ const messages = this.convertMessages(request.messages);
75
+
76
+ const stream = await this.client.chat.completions.create({
77
+ model: request.model || this.defaultModel,
78
+ messages,
79
+ max_tokens: request.maxTokens,
80
+ temperature: request.temperature,
81
+ stop: request.stopSequences,
82
+ stream: true,
83
+ });
84
+
85
+ // Accumulator for tool calls across chunks
86
+ const toolCallAccumulator = new Map<number, { id: string; name: string; arguments: string }>();
87
+ let usage: StreamChunk['usage'] | undefined;
88
+
89
+ for await (const chunk of stream) {
90
+ const delta = chunk.choices[0]?.delta;
91
+ const finishReason = chunk.choices[0]?.finish_reason;
92
+
93
+ if (delta?.content) {
94
+ yield {
95
+ content: delta.content,
96
+ done: false,
97
+ };
98
+ }
99
+
100
+ if (delta?.tool_calls) {
101
+ // Accumulate tool calls across chunks
102
+ for (const tc of delta.tool_calls) {
103
+ const index = tc.index ?? 0;
104
+ const existing = toolCallAccumulator.get(index);
105
+
106
+ if (existing) {
107
+ // Append arguments to existing tool call
108
+ if (tc.function?.arguments) {
109
+ existing.arguments += tc.function.arguments;
110
+ }
111
+ } else {
112
+ // Initialize new tool call entry
113
+ toolCallAccumulator.set(index, {
114
+ id: tc.id || '',
115
+ name: tc.function?.name || '',
116
+ arguments: tc.function?.arguments || '',
117
+ });
118
+ }
119
+ }
120
+ }
121
+
122
+ // Accumulate usage (do not yield until done)
123
+ if (chunk.usage) {
124
+ usage = {
125
+ promptTokens: chunk.usage.prompt_tokens || 0,
126
+ completionTokens: chunk.usage.completion_tokens || 0,
127
+ totalTokens: chunk.usage.total_tokens || 0,
128
+ };
129
+ }
130
+
131
+ if (finishReason) {
132
+ const toolCalls =
133
+ toolCallAccumulator.size > 0
134
+ ? Array.from(toolCallAccumulator.values()).map(tc => ({
135
+ id: tc.id,
136
+ type: 'function' as const,
137
+ function: { name: tc.name, arguments: tc.arguments },
138
+ }))
139
+ : undefined;
140
+ yield { done: true, toolCalls, usage };
141
+ return;
142
+ }
143
+ }
144
+ }
145
+
146
+ async completeWithTools(request: ToolCompletionRequest): Promise<LLMResponse> {
147
+ const messages = this.convertMessages(request.messages);
148
+
149
+ const response = await this.client.chat.completions.create({
150
+ model: request.model || this.defaultModel,
151
+ messages,
152
+ tools: request.tools.map(t => ({
153
+ type: 'function' as const,
154
+ function: {
155
+ name: t.function.name,
156
+ description: t.function.description,
157
+ parameters: t.function.parameters,
158
+ },
159
+ })),
160
+ tool_choice: request.toolChoice,
161
+ max_tokens: request.maxTokens,
162
+ temperature: request.temperature,
163
+ });
164
+
165
+ const choice = response.choices?.[0];
166
+ if (!choice) {
167
+ throw new Error(`${this.name} response missing choices`);
168
+ }
169
+
170
+ return {
171
+ content: choice.message.content || '',
172
+ toolCalls: choice.message.tool_calls?.map(tc => this.convertToolCall(tc)),
173
+ usage: {
174
+ promptTokens: response.usage?.prompt_tokens || 0,
175
+ completionTokens: response.usage?.completion_tokens || 0,
176
+ totalTokens: response.usage?.total_tokens || 0,
177
+ },
178
+ model: response.model,
179
+ finishReason: this.mapFinishReason(choice.finish_reason),
180
+ };
181
+ }
182
+
183
+ /**
184
+ * Stream a chat completion with tool calling support.
185
+ * Uses the OpenAI SDK streaming interface with tools, accumulating tool calls
186
+ * and yielding text deltas incrementally.
187
+ */
188
+ async *streamWithTools(request: ToolCompletionRequest): AsyncGenerator<StreamChunk> {
189
+ const messages = this.convertMessages(request.messages);
190
+
191
+ const stream = await this.client.chat.completions.create({
192
+ model: request.model || this.defaultModel,
193
+ messages,
194
+ tools: request.tools.map(t => ({
195
+ type: 'function' as const,
196
+ function: {
197
+ name: t.function.name,
198
+ description: t.function.description,
199
+ parameters: t.function.parameters,
200
+ },
201
+ })),
202
+ tool_choice: request.toolChoice,
203
+ max_tokens: request.maxTokens,
204
+ temperature: request.temperature,
205
+ stream: true,
206
+ stream_options: { include_usage: true },
207
+ });
208
+
209
+ const toolCallAccumulator = new Map<number, { id: string; name: string; arguments: string }>();
210
+ let usage: StreamChunk['usage'] | undefined;
211
+
212
+ for await (const chunk of stream) {
213
+ const delta = chunk.choices[0]?.delta;
214
+ const finishReason = chunk.choices[0]?.finish_reason;
215
+
216
+ if (delta?.content) {
217
+ yield { content: delta.content, done: false };
218
+ }
219
+
220
+ if (delta?.tool_calls) {
221
+ for (const tc of delta.tool_calls) {
222
+ const index = tc.index ?? 0;
223
+ const existing = toolCallAccumulator.get(index);
224
+ if (existing) {
225
+ if (tc.function?.arguments) {
226
+ existing.arguments += tc.function.arguments;
227
+ }
228
+ } else {
229
+ toolCallAccumulator.set(index, {
230
+ id: tc.id || '',
231
+ name: tc.function?.name || '',
232
+ arguments: tc.function?.arguments || '',
233
+ });
234
+ }
235
+ }
236
+ }
237
+
238
+ if (chunk.usage) {
239
+ usage = {
240
+ promptTokens: chunk.usage.prompt_tokens || 0,
241
+ completionTokens: chunk.usage.completion_tokens || 0,
242
+ totalTokens: chunk.usage.total_tokens || 0,
243
+ };
244
+ }
245
+
246
+ if (finishReason) {
247
+ const toolCalls =
248
+ toolCallAccumulator.size > 0
249
+ ? Array.from(toolCallAccumulator.values()).map(tc => ({
250
+ id: tc.id,
251
+ type: 'function' as const,
252
+ function: { name: tc.name, arguments: tc.arguments },
253
+ }))
254
+ : undefined;
255
+ yield { done: true, toolCalls, usage };
256
+ return;
257
+ }
258
+ }
259
+ }
260
+
261
+ async countTokens(text: string): Promise<number> {
262
+ // Approximation: ~4 characters per token
263
+ return Math.ceil(text.length / 4);
264
+ }
265
+
266
+ getMaxTokens(_model: string): number {
267
+ return 4096;
268
+ }
269
+
270
+ async listModels(): Promise<string[]> {
271
+ try {
272
+ const response = await this.client.models.list();
273
+ const ids: string[] = [];
274
+ for await (const model of response) {
275
+ if (model.id) {
276
+ ids.push(model.id);
277
+ }
278
+ }
279
+ if (ids.length > 0) {
280
+ return ids;
281
+ }
282
+ } catch {
283
+ // Server does not support /v1/models or is unreachable -- fall back
284
+ }
285
+ return [this.defaultModel];
286
+ }
287
+
288
+ /**
289
+ * Convert messages to OpenAI-compatible format
290
+ */
291
+ private convertMessages(messages: LLMMessage[]): OpenAI.ChatCompletionMessageParam[] {
292
+ return messages.map(m => {
293
+ if (m.role === 'tool') {
294
+ // Tool result message
295
+ return {
296
+ role: 'tool' as const,
297
+ content: getTextContent(m.content),
298
+ tool_call_id: m.toolCallId!,
299
+ };
300
+ }
301
+
302
+ if (m.role === 'assistant' && m.toolCalls && m.toolCalls.length > 0) {
303
+ // Assistant message with tool calls
304
+ return {
305
+ role: 'assistant' as const,
306
+ content: getTextContent(m.content) || null,
307
+ tool_calls: m.toolCalls.map(tc => ({
308
+ id: tc.id,
309
+ type: 'function' as const,
310
+ function: {
311
+ name: tc.function.name,
312
+ arguments: tc.function.arguments,
313
+ },
314
+ })),
315
+ };
316
+ }
317
+
318
+ // Regular message
319
+ return {
320
+ role: m.role as 'system' | 'user' | 'assistant',
321
+ content: getTextContent(m.content),
322
+ };
323
+ });
324
+ }
325
+
326
+ /**
327
+ * Convert OpenAI tool call to standard format
328
+ */
329
+ private convertToolCall(tc: OpenAI.Chat.Completions.ChatCompletionMessageToolCall): ToolCall {
330
+ const fn = 'function' in tc ? tc.function : { name: '', arguments: '{}' };
331
+ return {
332
+ id: tc.id,
333
+ type: 'function',
334
+ function: {
335
+ name: fn.name,
336
+ arguments: fn.arguments,
337
+ },
338
+ };
339
+ }
340
+ }
@@ -0,0 +1,328 @@
1
+ /**
2
+ * OpenAI Provider
3
+ * Supports GPT-4o, GPT-4o-mini, GPT-4-turbo
4
+ */
5
+
6
+ import OpenAI from 'openai';
7
+ import { encode } from 'gpt-tokenizer';
8
+ import {
9
+ BaseProvider,
10
+ getTextContent,
11
+ type CompletionRequest,
12
+ type LLMMessage,
13
+ type LLMResponse,
14
+ type StreamChunk,
15
+ type ToolCall,
16
+ type ToolCompletionRequest,
17
+ } from '../types';
18
+ import { getProviderApiKey } from '../auth-bridge';
19
+
20
+ export class OpenAIProvider extends BaseProvider {
21
+ name = 'openai';
22
+ private client: OpenAI;
23
+ private defaultModel = 'gpt-4o';
24
+
25
+ constructor(apiKey?: string) {
26
+ super();
27
+ this.client = new OpenAI({
28
+ apiKey: apiKey || getProviderApiKey('openai') || process.env.OPENAI_API_KEY,
29
+ });
30
+ }
31
+
32
+ async complete(request: CompletionRequest): Promise<LLMResponse> {
33
+ const messages = this.convertMessages(request.messages);
34
+
35
+ const response = await this.client.chat.completions.create({
36
+ model: request.model || this.defaultModel,
37
+ messages,
38
+ max_tokens: request.maxTokens,
39
+ temperature: request.temperature,
40
+ stop: request.stopSequences,
41
+ response_format: request.responseFormat,
42
+ });
43
+
44
+ const choice = response.choices?.[0];
45
+ if (!choice) {
46
+ throw new Error('OpenAI response missing choices');
47
+ }
48
+
49
+ return {
50
+ content: choice.message.content || '',
51
+ toolCalls: choice.message.tool_calls?.map(tc => this.convertToolCall(tc)),
52
+ usage: {
53
+ promptTokens: response.usage?.prompt_tokens || 0,
54
+ completionTokens: response.usage?.completion_tokens || 0,
55
+ totalTokens: response.usage?.total_tokens || 0,
56
+ },
57
+ model: response.model,
58
+ finishReason: this.mapFinishReason(choice.finish_reason),
59
+ };
60
+ }
61
+
62
+ async *stream(request: CompletionRequest): AsyncIterable<StreamChunk> {
63
+ const messages = this.convertMessages(request.messages);
64
+
65
+ const stream = await this.client.chat.completions.create({
66
+ model: request.model || this.defaultModel,
67
+ messages,
68
+ max_tokens: request.maxTokens,
69
+ temperature: request.temperature,
70
+ stop: request.stopSequences,
71
+ stream: true,
72
+ stream_options: { include_usage: true },
73
+ });
74
+
75
+ // Accumulator for tool calls across chunks
76
+ const toolCallAccumulator = new Map<number, { id: string; name: string; arguments: string }>();
77
+ let usage: StreamChunk['usage'] | undefined;
78
+
79
+ for await (const chunk of stream) {
80
+ const delta = chunk.choices[0]?.delta;
81
+ const finishReason = chunk.choices[0]?.finish_reason;
82
+
83
+ if (delta?.content) {
84
+ yield {
85
+ content: delta.content,
86
+ done: false,
87
+ };
88
+ }
89
+
90
+ if (delta?.tool_calls) {
91
+ // Accumulate tool calls across chunks (do not yield until done)
92
+ for (const tc of delta.tool_calls) {
93
+ const index = tc.index ?? 0;
94
+ const existing = toolCallAccumulator.get(index);
95
+
96
+ if (existing) {
97
+ // Append arguments to existing tool call
98
+ if (tc.function?.arguments) {
99
+ existing.arguments += tc.function.arguments;
100
+ }
101
+ } else {
102
+ // Initialize new tool call entry
103
+ toolCallAccumulator.set(index, {
104
+ id: tc.id || '',
105
+ name: tc.function?.name || '',
106
+ arguments: tc.function?.arguments || '',
107
+ });
108
+ }
109
+ }
110
+ }
111
+
112
+ // Accumulate usage (do not yield until done)
113
+ if (chunk.usage) {
114
+ usage = {
115
+ promptTokens: chunk.usage.prompt_tokens || 0,
116
+ completionTokens: chunk.usage.completion_tokens || 0,
117
+ totalTokens: chunk.usage.total_tokens || 0,
118
+ };
119
+ }
120
+
121
+ if (finishReason) {
122
+ const toolCalls =
123
+ toolCallAccumulator.size > 0
124
+ ? Array.from(toolCallAccumulator.values()).map(tc => ({
125
+ id: tc.id,
126
+ type: 'function' as const,
127
+ function: { name: tc.name, arguments: tc.arguments },
128
+ }))
129
+ : undefined;
130
+ yield { done: true, toolCalls, usage };
131
+ return;
132
+ }
133
+ }
134
+ }
135
+
136
+ async completeWithTools(request: ToolCompletionRequest): Promise<LLMResponse> {
137
+ const messages = this.convertMessages(request.messages);
138
+
139
+ const response = await this.client.chat.completions.create({
140
+ model: request.model || this.defaultModel,
141
+ messages,
142
+ tools: request.tools.map(t => ({
143
+ type: 'function' as const,
144
+ function: {
145
+ name: t.function.name,
146
+ description: t.function.description,
147
+ parameters: t.function.parameters,
148
+ },
149
+ })),
150
+ tool_choice: request.toolChoice,
151
+ max_tokens: request.maxTokens,
152
+ temperature: request.temperature,
153
+ });
154
+
155
+ const choice = response.choices?.[0];
156
+ if (!choice) {
157
+ throw new Error('OpenAI response missing choices');
158
+ }
159
+
160
+ return {
161
+ content: choice.message.content || '',
162
+ toolCalls: choice.message.tool_calls?.map(tc => this.convertToolCall(tc)),
163
+ usage: {
164
+ promptTokens: response.usage?.prompt_tokens || 0,
165
+ completionTokens: response.usage?.completion_tokens || 0,
166
+ totalTokens: response.usage?.total_tokens || 0,
167
+ },
168
+ model: response.model,
169
+ finishReason: this.mapFinishReason(choice.finish_reason),
170
+ };
171
+ }
172
+
173
+ async *streamWithTools(request: ToolCompletionRequest): AsyncIterable<StreamChunk> {
174
+ const messages = this.convertMessages(request.messages);
175
+
176
+ const stream = await this.client.chat.completions.create({
177
+ model: request.model || this.defaultModel,
178
+ messages,
179
+ tools: request.tools.map(t => ({
180
+ type: 'function' as const,
181
+ function: {
182
+ name: t.function.name,
183
+ description: t.function.description,
184
+ parameters: t.function.parameters,
185
+ },
186
+ })),
187
+ tool_choice: request.toolChoice,
188
+ max_tokens: request.maxTokens,
189
+ temperature: request.temperature,
190
+ stream: true,
191
+ stream_options: { include_usage: true },
192
+ });
193
+
194
+ const toolCallAccumulator = new Map<number, { id: string; name: string; arguments: string }>();
195
+ let usage: StreamChunk['usage'] | undefined;
196
+
197
+ for await (const chunk of stream) {
198
+ const delta = chunk.choices[0]?.delta;
199
+ const finishReason = chunk.choices[0]?.finish_reason;
200
+
201
+ if (delta?.content) {
202
+ yield { content: delta.content, done: false };
203
+ }
204
+
205
+ if (delta?.tool_calls) {
206
+ for (const tc of delta.tool_calls) {
207
+ const index = tc.index ?? 0;
208
+ const existing = toolCallAccumulator.get(index);
209
+ if (existing) {
210
+ if (tc.function?.arguments) {
211
+ existing.arguments += tc.function.arguments;
212
+ }
213
+ } else {
214
+ toolCallAccumulator.set(index, {
215
+ id: tc.id || '',
216
+ name: tc.function?.name || '',
217
+ arguments: tc.function?.arguments || '',
218
+ });
219
+ }
220
+ }
221
+ }
222
+
223
+ if (chunk.usage) {
224
+ usage = {
225
+ promptTokens: chunk.usage.prompt_tokens || 0,
226
+ completionTokens: chunk.usage.completion_tokens || 0,
227
+ totalTokens: chunk.usage.total_tokens || 0,
228
+ };
229
+ }
230
+
231
+ if (finishReason) {
232
+ const toolCalls =
233
+ toolCallAccumulator.size > 0
234
+ ? Array.from(toolCallAccumulator.values()).map(tc => ({
235
+ id: tc.id,
236
+ type: 'function' as const,
237
+ function: { name: tc.name, arguments: tc.arguments },
238
+ }))
239
+ : undefined;
240
+ yield { done: true, toolCalls, usage };
241
+ return;
242
+ }
243
+ }
244
+ }
245
+
246
+ async countTokens(text: string): Promise<number> {
247
+ // Use gpt-tokenizer for accurate count
248
+ return encode(text).length;
249
+ }
250
+
251
+ getMaxTokens(model: string): number {
252
+ const limits: Record<string, number> = {
253
+ 'gpt-4o': 16384,
254
+ 'gpt-4o-2024-11-20': 16384,
255
+ 'gpt-4o-mini': 16384,
256
+ 'gpt-4o-mini-2024-07-18': 16384,
257
+ 'gpt-4-turbo': 4096,
258
+ 'gpt-4-turbo-2024-04-09': 4096,
259
+ 'gpt-4': 8192,
260
+ 'gpt-3.5-turbo': 4096,
261
+ };
262
+ return limits[model] || 4096;
263
+ }
264
+
265
+ async listModels(): Promise<string[]> {
266
+ return [
267
+ 'gpt-4o',
268
+ 'gpt-4o-2024-11-20',
269
+ 'gpt-4o-mini',
270
+ 'gpt-4o-mini-2024-07-18',
271
+ 'gpt-4-turbo',
272
+ 'gpt-4',
273
+ ];
274
+ }
275
+
276
+ /**
277
+ * Convert messages to OpenAI format
278
+ */
279
+ private convertMessages(messages: LLMMessage[]): OpenAI.ChatCompletionMessageParam[] {
280
+ return messages.map(m => {
281
+ if (m.role === 'tool') {
282
+ // Tool result message
283
+ return {
284
+ role: 'tool' as const,
285
+ content: getTextContent(m.content),
286
+ tool_call_id: m.toolCallId!,
287
+ };
288
+ }
289
+
290
+ if (m.role === 'assistant' && m.toolCalls && m.toolCalls.length > 0) {
291
+ // Assistant message with tool calls
292
+ return {
293
+ role: 'assistant' as const,
294
+ content: getTextContent(m.content) || null,
295
+ tool_calls: m.toolCalls.map(tc => ({
296
+ id: tc.id,
297
+ type: 'function' as const,
298
+ function: {
299
+ name: tc.function.name,
300
+ arguments: tc.function.arguments,
301
+ },
302
+ })),
303
+ };
304
+ }
305
+
306
+ // Regular message
307
+ return {
308
+ role: m.role as 'system' | 'user' | 'assistant',
309
+ content: getTextContent(m.content),
310
+ };
311
+ });
312
+ }
313
+
314
+ /**
315
+ * Convert OpenAI tool call to standard format
316
+ */
317
+ private convertToolCall(tc: OpenAI.Chat.Completions.ChatCompletionMessageToolCall): ToolCall {
318
+ const fn = 'function' in tc ? tc.function : { name: '', arguments: '{}' };
319
+ return {
320
+ id: tc.id,
321
+ type: 'function',
322
+ function: {
323
+ name: fn.name,
324
+ arguments: fn.arguments,
325
+ },
326
+ };
327
+ }
328
+ }