novacode 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.
package/src/main.ts ADDED
@@ -0,0 +1,169 @@
1
+ /**
2
+ * Entry point for the novacode CLI.
3
+ * Handles configuration, CLI flags, and switches between interactive/print modes.
4
+ */
5
+ import { parseArgs } from "node:util"
6
+ import { Agent } from "./agent/agent.ts"
7
+ import { buildSystemPrompt } from "./agent/prompt.ts"
8
+ import { handleSessionCommand } from "./commands/session.ts"
9
+ import { getProvider, MODELS } from "./config/providers.ts"
10
+ import { configExists, loadAuth, loadConfig } from "./config/store.ts"
11
+ import { runOnboarding } from "./onboarding/wizard.ts"
12
+ import { getSessionStore } from "./session/store.ts"
13
+ import { getAllTools } from "./tools/index.ts"
14
+ import { runPrintMode } from "./tui/print.ts"
15
+
16
+ // Ensure providers are registered
17
+ import "./provider/openai.ts"
18
+ import "./provider/gemini.ts"
19
+
20
+ function parseCli() {
21
+ const { values, positionals } = parseArgs({
22
+ options: {
23
+ help: { type: "boolean", short: "h" },
24
+ version: { type: "boolean", short: "v" },
25
+ provider: { type: "string" },
26
+ model: { type: "string" },
27
+ "api-key": { type: "string" },
28
+ session: { type: "string", short: "s" },
29
+ },
30
+ strict: false,
31
+ allowPositionals: true,
32
+ })
33
+
34
+ return { flags: values, args: positionals }
35
+ }
36
+
37
+ function findModel(modelId: string, providerId?: string) {
38
+ return MODELS.find((m) => {
39
+ if (providerId) return m.provider === providerId && m.id === modelId
40
+ return m.id === modelId
41
+ })
42
+ }
43
+
44
+ async function main() {
45
+ const { flags, args } = parseCli()
46
+
47
+ if (flags.version) {
48
+ const pkg = await Bun.file("package.json").json()
49
+ console.log(`novacode ${pkg.version}`)
50
+ process.exit(0)
51
+ }
52
+
53
+ if (flags.help) {
54
+ console.log(`novacode — open-source coding agent
55
+
56
+ Usage:
57
+ novacode Interactive mode
58
+ novacode "prompt" Print mode (non-interactive)
59
+ novacode session <cmd> Session management (list, delete)
60
+ novacode --session <id> Resume a session
61
+
62
+ Options:
63
+ -h, --help Show help
64
+ -v, --version Show version
65
+ --provider <id> Provider to use
66
+ --model <id> Model to use
67
+ --api-key <key> API key override
68
+ -s, --session <id> Resume session by ID`)
69
+ process.exit(0)
70
+ }
71
+
72
+ // Handle session subcommand
73
+ if (args[0] === "session") {
74
+ await handleSessionCommand(args.slice(1))
75
+ return
76
+ }
77
+
78
+ const controller = new AbortController()
79
+
80
+ const onSignal = () => {
81
+ controller.abort()
82
+ process.stderr.write("\nAborted.\n")
83
+ process.exit(130)
84
+ }
85
+ process.on("SIGINT", onSignal)
86
+ process.on("SIGTERM", onSignal)
87
+
88
+ // First-run onboarding
89
+ const config = await ((await configExists()) ? loadConfig() : runOnboarding())
90
+ const auth = await loadAuth()
91
+
92
+ // CLI overrides
93
+ const providerId = (flags.provider as string) || config.provider
94
+ const modelId = (flags.model as string) || config.model
95
+ const apiKey = (flags["api-key"] as string) || auth.apiKeys[providerId]
96
+
97
+ const provider = getProvider(providerId)
98
+ if (!provider) {
99
+ console.error(`Unknown provider: ${providerId}`)
100
+ console.error(`Available: ${getProvider("glm") ? "glm, " : ""}gemini, deepseek, openai`)
101
+ process.exit(1)
102
+ }
103
+
104
+ if (!apiKey) {
105
+ console.error(
106
+ `No API key for ${provider.name}. Set ${provider.envKey} or run novacode for onboarding.`,
107
+ )
108
+ process.exit(1)
109
+ }
110
+
111
+ const model = findModel(modelId, providerId)
112
+ if (!model) {
113
+ console.error(`Unknown model: ${modelId}`)
114
+ console.error("Available models:")
115
+ for (const m of MODELS.filter((m) => m.provider === providerId)) {
116
+ console.error(` ${m.id} — ${m.name}`)
117
+ }
118
+ process.exit(1)
119
+ }
120
+
121
+ const cwd = process.cwd()
122
+ const tools = getAllTools(cwd)
123
+ const system = buildSystemPrompt(cwd, tools)
124
+
125
+ // Session persistence
126
+ const store = getSessionStore()
127
+ const session = flags.session
128
+ ? store.get(flags.session as string)
129
+ : store.create(cwd, model.id, providerId)
130
+
131
+ if (flags.session && !session) {
132
+ console.error(`Session not found: ${flags.session}`)
133
+ process.exit(1)
134
+ }
135
+
136
+ const sessionId = session!.id
137
+ const existingMessages = store.messages(sessionId)
138
+
139
+ const agent = new Agent({
140
+ api: provider.api,
141
+ model,
142
+ apiKey,
143
+ baseUrl: provider.baseUrl,
144
+ system,
145
+ tools,
146
+ messages: existingMessages,
147
+ })
148
+
149
+ // Print mode: prompt provided as arg
150
+ const prompt = args.join(" ")
151
+ if (prompt) {
152
+ const result = await runPrintMode(agent, prompt, controller.signal)
153
+ if (result) {
154
+ store.appendMany(sessionId, result)
155
+ }
156
+ return
157
+ }
158
+
159
+ // Interactive TUI mode (Phase 3)
160
+ process.off("SIGINT", onSignal)
161
+ process.off("SIGTERM", onSignal)
162
+ const { interactive } = await import("./tui/app.tsx")
163
+ await interactive(agent, store, sessionId)
164
+ }
165
+
166
+ main().catch((e) => {
167
+ console.error("Fatal:", e.message)
168
+ process.exit(1)
169
+ })
@@ -0,0 +1,58 @@
1
+ import * as clack from "@clack/prompts"
2
+ import { getModelsForProvider, getProvider, PROVIDERS } from "../config/providers.ts"
3
+ import { saveAuth, saveConfig } from "../config/store.ts"
4
+ import type { NovaConfig } from "../types.ts"
5
+
6
+ export async function runOnboarding(): Promise<NovaConfig> {
7
+ clack.intro("⚡ Nova — your coding companion")
8
+
9
+ const providerId = await clack.select({
10
+ message: "Pick a provider",
11
+ options: PROVIDERS.map((p) => ({ value: p.id, label: p.name })),
12
+ })
13
+
14
+ if (clack.isCancel(providerId)) {
15
+ clack.cancel("Cancelled")
16
+ process.exit(0)
17
+ }
18
+
19
+ const provider = getProvider(providerId as string)
20
+ if (!provider) {
21
+ clack.cancel("Unknown provider")
22
+ process.exit(1)
23
+ }
24
+
25
+ const apiKey = await clack.password({
26
+ message: `Enter ${provider.name} API key`,
27
+ })
28
+
29
+ if (clack.isCancel(apiKey)) {
30
+ clack.cancel("Cancelled")
31
+ process.exit(0)
32
+ }
33
+
34
+ const models = getModelsForProvider(providerId as string)
35
+ const modelId = await clack.select({
36
+ message: "Pick a default model",
37
+ options: models.map((m) => ({
38
+ value: m.id,
39
+ label: `${m.name} (${(m.contextWindow / 1000).toFixed(0)}k ctx)`,
40
+ })),
41
+ })
42
+
43
+ if (clack.isCancel(modelId)) {
44
+ clack.cancel("Cancelled")
45
+ process.exit(0)
46
+ }
47
+
48
+ const config: NovaConfig = {
49
+ provider: providerId as string,
50
+ model: modelId as string,
51
+ }
52
+
53
+ await saveConfig(config)
54
+ await saveAuth({ apiKeys: { [providerId as string]: apiKey as string } })
55
+
56
+ clack.note("Ready. Type your prompt or /help for commands")
57
+ return config
58
+ }
@@ -0,0 +1,254 @@
1
+ import type {
2
+ AssistantResult,
3
+ ContentPart,
4
+ Msg,
5
+ StopReason,
6
+ StreamEvent,
7
+ StreamFn,
8
+ StreamOpts,
9
+ ToolDef,
10
+ Usage,
11
+ } from "../types.ts"
12
+ import { register } from "./registry.ts"
13
+ import { EventStream } from "./stream.ts"
14
+
15
+ interface GeminiPart {
16
+ text?: string
17
+ thought?: boolean | string
18
+ inline_data?: { mime_type: string; data: string }
19
+ function_call?: { name: string; args: Record<string, unknown> }
20
+ function_response?: { name: string; response: Record<string, unknown> }
21
+ thought_signature?: string
22
+ }
23
+
24
+ interface GeminiContent {
25
+ role: "user" | "model"
26
+ parts: GeminiPart[]
27
+ }
28
+
29
+ /**
30
+ * Maps our internal Msg format to the Gemini 'Content' format.
31
+ * Groups consecutive tool_result messages into a single Gemini message.
32
+ */
33
+ function msgsToGemini(messages: Msg[]): GeminiContent[] {
34
+ const contents: GeminiContent[] = []
35
+
36
+ for (const msg of messages) {
37
+ if (msg.role === "user") {
38
+ const parts: GeminiPart[] =
39
+ typeof msg.content === "string"
40
+ ? [{ text: msg.content }]
41
+ : msg.content.map((c) => {
42
+ if (c.type === "text") return { text: c.text }
43
+ if (c.type === "image") return { inline_data: { mime_type: c.mime, data: c.data } }
44
+ return { text: "" }
45
+ })
46
+ contents.push({ role: "user", parts })
47
+ } else if (msg.role === "assistant") {
48
+ const parts: GeminiPart[] = msg.content.map((c) => {
49
+ if (c.type === "text") return { text: c.text, thought_signature: c.signature }
50
+ if (c.type === "thinking")
51
+ return { thought: true, text: c.text, thought_signature: c.signature }
52
+ if (c.type === "tool_call")
53
+ return { function_call: { name: c.name, args: c.args }, thought_signature: c.signature }
54
+ return { text: "" }
55
+ })
56
+ contents.push({ role: "model", parts })
57
+ } else if (msg.role === "tool_result") {
58
+ const part: GeminiPart = {
59
+ function_response: {
60
+ name: msg.tool,
61
+ response: {
62
+ content: msg.content
63
+ .map((c) => (c.type === "text" ? c.text : JSON.stringify(c)))
64
+ .join("\n"),
65
+ },
66
+ },
67
+ }
68
+
69
+ const last = contents[contents.length - 1]
70
+ // Gemini requires alternating roles; multiple function_responses group into one 'user' message.
71
+ if (last && last.role === "user" && last.parts.some((p) => p.function_response)) {
72
+ last.parts.push(part)
73
+ } else {
74
+ contents.push({ role: "user", parts: [part] })
75
+ }
76
+ }
77
+ }
78
+
79
+ return contents
80
+ }
81
+
82
+ function toolsToGemini(tools: ToolDef[]): unknown[] {
83
+ if (tools.length === 0) return []
84
+ return [
85
+ {
86
+ function_declarations: tools.map((t) => ({
87
+ name: t.name,
88
+ description: t.description,
89
+ parameters: t.parameters,
90
+ })),
91
+ },
92
+ ]
93
+ }
94
+
95
+ export const streamGemini: StreamFn = (
96
+ opts: StreamOpts,
97
+ ): EventStream<StreamEvent, AssistantResult> => {
98
+ const es = new EventStream<StreamEvent, AssistantResult>()
99
+
100
+ ;(async () => {
101
+ try {
102
+ const baseUrl = opts.baseUrl || "https://generativelanguage.googleapis.com"
103
+ const url = `${baseUrl}/v1beta/models/${opts.model.id}:streamGenerateContent?alt=sse&key=${opts.apiKey}`
104
+
105
+ const body = {
106
+ contents: msgsToGemini(opts.messages),
107
+ system_instruction: opts.system ? { parts: [{ text: opts.system }] } : undefined,
108
+ tools: opts.tools.length > 0 ? toolsToGemini(opts.tools) : undefined,
109
+ generationConfig: {
110
+ thinkingConfig: opts.model.supportsThinking ? { thinkingLevel: "low" } : undefined,
111
+ },
112
+ }
113
+
114
+ const response = await fetch(url, {
115
+ method: "POST",
116
+ headers: {
117
+ "Content-Type": "application/json",
118
+ "Api-Revision": "2026-05-20",
119
+ },
120
+ body: JSON.stringify(body),
121
+ signal: opts.signal,
122
+ })
123
+
124
+ if (!response.ok) {
125
+ const text = await response.text()
126
+ let msg = text
127
+ try {
128
+ const json = JSON.parse(text)
129
+ msg = json.error?.message || json.message || text
130
+ } catch {
131
+ /* use raw text */
132
+ }
133
+
134
+ const errorMsg = `Gemini Error (${response.status}): ${msg}`
135
+ es.push({ type: "text_delta", text: errorMsg })
136
+ es.finish({
137
+ content: [{ type: "text", text: errorMsg }],
138
+ usage: { in: 0, out: 0 },
139
+ stop: "error",
140
+ })
141
+ return
142
+ }
143
+
144
+ const reader = response.body?.getReader()
145
+ if (!reader) {
146
+ es.finish({ content: [], usage: { in: 0, out: 0 }, stop: "error" })
147
+ return
148
+ }
149
+
150
+ const decoder = new TextDecoder()
151
+ let buffer = ""
152
+ let usage: Usage = { in: 0, out: 0 }
153
+ let stop: StopReason = "stop"
154
+ const content: ContentPart[] = []
155
+
156
+ while (true) {
157
+ const { done, value } = await reader.read()
158
+ if (done) break
159
+
160
+ buffer += decoder.decode(value, { stream: true })
161
+ const lines = buffer.split("\n")
162
+ buffer = lines.pop() ?? ""
163
+
164
+ for (const line of lines) {
165
+ const trimmed = line.trim()
166
+ if (!trimmed?.startsWith("data: ")) continue
167
+ const data = trimmed.slice(6)
168
+
169
+ try {
170
+ const chunk = JSON.parse(data)
171
+ const candidate = chunk.candidates?.[0]
172
+
173
+ // Handle usage metadata
174
+ if (chunk.usageMetadata) {
175
+ usage = {
176
+ in: chunk.usageMetadata.promptTokenCount || usage.in,
177
+ out: chunk.usageMetadata.candidatesTokenCount || usage.out,
178
+ }
179
+ es.push({ type: "usage", usage })
180
+ }
181
+
182
+ if (!candidate) continue
183
+
184
+ // Map finish reason
185
+ if (candidate.finishReason) {
186
+ const reason = candidate.finishReason
187
+ if (reason === "STOP") stop = "stop"
188
+ else if (reason === "MAX_TOKENS") stop = "length"
189
+ else if (reason === "SAFETY" || reason === "RECITATION" || reason === "OTHER")
190
+ stop = "error"
191
+ }
192
+
193
+ const parts = candidate.content?.parts
194
+ if (parts) {
195
+ for (const part of parts) {
196
+ const sig = part.thought_signature || part.thoughtSignature
197
+
198
+ // Handle text and thinking deltas
199
+ if (part.text) {
200
+ if (part.thought === true || typeof part.thought === "string") {
201
+ const thoughtText = typeof part.thought === "string" ? part.thought : part.text
202
+ es.push({ type: "thinking_delta", text: thoughtText })
203
+ content.push({ type: "thinking", text: thoughtText, signature: sig })
204
+ } else {
205
+ es.push({ type: "text_delta", text: part.text })
206
+ content.push({ type: "text", text: part.text, signature: sig })
207
+ }
208
+ }
209
+
210
+ // Handle function calls (can be snake_case or camelCase in some API versions)
211
+ const fc = part.functionCall || part.function_call
212
+ if (fc) {
213
+ const name = fc.name
214
+ const args = (fc.args as Record<string, unknown>) || {}
215
+ const id = `call_${Math.random().toString(36).slice(2, 9)}`
216
+
217
+ const toolCall: ContentPart = {
218
+ type: "tool_call",
219
+ id,
220
+ name,
221
+ args,
222
+ signature: sig,
223
+ }
224
+ content.push(toolCall)
225
+ es.push({ type: "tool_call", call: toolCall })
226
+ stop = "tool_use"
227
+ }
228
+ }
229
+ }
230
+ } catch (_e) {
231
+ if (data.trim() !== "" && data.trim() !== "[DONE]") {
232
+ // skip noise
233
+ }
234
+ }
235
+ }
236
+ }
237
+
238
+ es.finish({ content, usage, stop })
239
+ } catch (e) {
240
+ if (opts.signal?.aborted) return
241
+ const errorMsg = `Gemini Network/Request Error: ${e instanceof Error ? e.message : String(e)}`
242
+ es.push({ type: "text_delta", text: errorMsg })
243
+ es.finish({
244
+ content: [{ type: "text", text: errorMsg }],
245
+ usage: { in: 0, out: 0 },
246
+ stop: "error",
247
+ })
248
+ }
249
+ })()
250
+
251
+ return es
252
+ }
253
+
254
+ register("gemini", streamGemini)
@@ -0,0 +1,218 @@
1
+ import type {
2
+ AssistantResult,
3
+ ContentPart,
4
+ Msg,
5
+ StopReason,
6
+ StreamEvent,
7
+ StreamFn,
8
+ StreamOpts,
9
+ ToolDef,
10
+ Usage,
11
+ } from "../types.ts"
12
+ import { consolidate } from "../util.ts"
13
+ import { register } from "./registry.ts"
14
+ import { EventStream } from "./stream.ts"
15
+
16
+ function msgToOpenAI(msg: Msg): Record<string, unknown> {
17
+ if (msg.role === "user") {
18
+ return {
19
+ role: "user",
20
+ content:
21
+ typeof msg.content === "string"
22
+ ? msg.content
23
+ : msg.content.map((c) => {
24
+ if (c.type === "text") return { type: "text", text: c.text }
25
+ if (c.type === "image")
26
+ return { type: "image_url", image_url: { url: `data:${c.mime};base64,${c.data}` } }
27
+ return { type: "text", text: "" }
28
+ }),
29
+ }
30
+ }
31
+ if (msg.role === "assistant") {
32
+ const textParts: string[] = []
33
+ const toolCalls: unknown[] = []
34
+
35
+ for (const c of msg.content) {
36
+ if (c.type === "text") textParts.push(c.text)
37
+ // thinking parts are internal — never sent back to the API
38
+ if (c.type === "tool_call")
39
+ toolCalls.push({
40
+ type: "function",
41
+ id: c.id,
42
+ function: { name: c.name, arguments: JSON.stringify(c.args) },
43
+ })
44
+ }
45
+
46
+ const result: Record<string, unknown> = {
47
+ role: "assistant",
48
+ content: textParts.length > 0 ? textParts.join("") : null,
49
+ }
50
+ if (toolCalls.length > 0) result.tool_calls = toolCalls
51
+ return result
52
+ }
53
+ // tool_result
54
+ if (msg.role === "tool_result") {
55
+ return {
56
+ role: "tool",
57
+ tool_call_id: msg.callId,
58
+ content: msg.content.map((c) => (c.type === "text" ? c.text : JSON.stringify(c))).join("\n"),
59
+ }
60
+ }
61
+ return { role: "user", content: "" }
62
+ }
63
+
64
+ function toolsToOpenAI(tools: ToolDef[]): unknown[] {
65
+ return tools.map((t) => ({
66
+ type: "function",
67
+ function: {
68
+ name: t.name,
69
+ description: t.description,
70
+ parameters: t.parameters,
71
+ },
72
+ }))
73
+ }
74
+
75
+ export const streamOpenAI: StreamFn = (
76
+ opts: StreamOpts,
77
+ ): EventStream<StreamEvent, AssistantResult> => {
78
+ const es = new EventStream<StreamEvent, AssistantResult>()
79
+
80
+ ;(async () => {
81
+ try {
82
+ const body = {
83
+ model: opts.model.id,
84
+ messages: [{ role: "system", content: opts.system }, ...opts.messages.map(msgToOpenAI)],
85
+ tools: opts.tools.length > 0 ? toolsToOpenAI(opts.tools) : undefined,
86
+ stream: true,
87
+ }
88
+
89
+ const response = await fetch(`${opts.baseUrl}/chat/completions`, {
90
+ method: "POST",
91
+ headers: {
92
+ "Content-Type": "application/json",
93
+ Authorization: `Bearer ${opts.apiKey}`,
94
+ },
95
+ body: JSON.stringify(body),
96
+ signal: opts.signal,
97
+ })
98
+
99
+ if (!response.ok) {
100
+ const text = await response.text()
101
+ const errorMsg = `API error ${response.status}: ${text}`
102
+ es.push({ type: "text_delta", text: errorMsg })
103
+ es.finish({
104
+ content: [{ type: "text", text: errorMsg }],
105
+ usage: { in: 0, out: 0 },
106
+ stop: "error",
107
+ })
108
+ return
109
+ }
110
+
111
+ const reader = response.body?.getReader()
112
+ if (!reader) {
113
+ es.finish({ content: [], usage: { in: 0, out: 0 }, stop: "error" })
114
+ return
115
+ }
116
+
117
+ const decoder = new TextDecoder()
118
+ let buffer = ""
119
+ const currentToolCalls = new Map<number, { id: string; name: string; args: string }>()
120
+ let usage: Usage = { in: 0, out: 0 }
121
+ let stop = "stop"
122
+ const textParts: ContentPart[] = []
123
+
124
+ while (true) {
125
+ const { done, value } = await reader.read()
126
+ if (done) break
127
+
128
+ buffer += decoder.decode(value, { stream: true })
129
+ const lines = buffer.split("\n")
130
+ buffer = lines.pop() ?? ""
131
+
132
+ for (const line of lines) {
133
+ const trimmed = line.trim()
134
+ if (!trimmed?.startsWith("data: ")) continue
135
+ const data = trimmed.slice(6)
136
+ if (data === "[DONE]") continue
137
+
138
+ try {
139
+ const chunk = JSON.parse(data)
140
+ const delta = chunk.choices?.[0]?.delta
141
+ if (!delta) continue
142
+
143
+ if (delta.content) {
144
+ es.push({ type: "text_delta", text: delta.content })
145
+ textParts.push({ type: "text", text: delta.content })
146
+ }
147
+
148
+ if (delta.tool_calls) {
149
+ for (const tc of delta.tool_calls) {
150
+ const idx = tc.index ?? 0
151
+ if (!currentToolCalls.has(idx)) {
152
+ currentToolCalls.set(idx, {
153
+ id: tc.id ?? "",
154
+ name: tc.function?.name ?? "",
155
+ args: "",
156
+ })
157
+ }
158
+ const existing = currentToolCalls.get(idx)!
159
+ if (tc.id) existing.id = tc.id
160
+ if (tc.function?.name) existing.name = tc.function.name
161
+ if (tc.function?.arguments) existing.args += tc.function.arguments
162
+ }
163
+ }
164
+
165
+ if (chunk.usage) {
166
+ usage = {
167
+ in: chunk.usage.prompt_tokens ?? 0,
168
+ out: chunk.usage.completion_tokens ?? 0,
169
+ }
170
+ es.push({ type: "usage", usage })
171
+ }
172
+
173
+ const finishReason = chunk.choices?.[0]?.finish_reason
174
+ if (finishReason) stop = finishReason
175
+ } catch {
176
+ // Skip malformed JSON chunks
177
+ }
178
+ }
179
+ }
180
+
181
+ const content: AssistantResult["content"] = consolidate([...textParts])
182
+ for (const [, tc] of currentToolCalls) {
183
+ content.push({
184
+ type: "tool_call",
185
+ id: tc.id,
186
+ name: tc.name,
187
+ args: JSON.parse(tc.args || "{}"),
188
+ })
189
+ es.push({
190
+ type: "tool_call",
191
+ call: {
192
+ type: "tool_call",
193
+ id: tc.id,
194
+ name: tc.name,
195
+ args: JSON.parse(tc.args || "{}"),
196
+ },
197
+ })
198
+ stop = "tool_use"
199
+ }
200
+
201
+ es.finish({ content, usage, stop: stop as StopReason })
202
+ } catch (e) {
203
+ if (opts.signal?.aborted) return
204
+ const errorMsg = `Unexpected error: ${e instanceof Error ? e.message : String(e)}`
205
+ es.push({ type: "text_delta", text: errorMsg })
206
+ es.finish({
207
+ content: [{ type: "text", text: errorMsg }],
208
+ usage: { in: 0, out: 0 },
209
+ stop: "error",
210
+ })
211
+ }
212
+ })()
213
+
214
+ return es
215
+ }
216
+
217
+ // Auto-register
218
+ register("openai", streamOpenAI)