novacode 0.6.0 → 0.7.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/dist/app-CbJSUNmf.mjs +22 -0
- package/dist/app-CbJSUNmf.mjs.map +1 -0
- package/dist/main.mjs +42 -29
- package/dist/main.mjs.map +1 -1
- package/package.json +1 -2
- package/dist/app-bQ9a_p_K.mjs +0 -22
- package/dist/app-bQ9a_p_K.mjs.map +0 -1
- package/src/agent/agent.ts +0 -87
- package/src/agent/loop.ts +0 -237
- package/src/agent/prompt.ts +0 -50
- package/src/commands/compact.ts +0 -28
- package/src/commands/index.ts +0 -128
- package/src/commands/models.ts +0 -85
- package/src/commands/providers.ts +0 -213
- package/src/commands/session.ts +0 -52
- package/src/config/providers.ts +0 -207
- package/src/config/store.ts +0 -66
- package/src/main.ts +0 -205
- package/src/onboarding/wizard.ts +0 -54
- package/src/provider/gemini.ts +0 -269
- package/src/provider/openai.ts +0 -239
- package/src/provider/stream.ts +0 -138
- package/src/session/compact.ts +0 -159
- package/src/session/store.ts +0 -209
- package/src/tools/fs.ts +0 -189
- package/src/tools/git.ts +0 -99
- package/src/tools/index.ts +0 -33
- package/src/tools/search.ts +0 -274
- package/src/tools/shell.ts +0 -90
- package/src/tools/web.ts +0 -239
- package/src/tui/app.tsx +0 -454
- package/src/tui/components/liveArea.tsx +0 -70
- package/src/tui/components/message.tsx +0 -117
- package/src/tui/components/statusBar.tsx +0 -64
- package/src/tui/constants.ts +0 -25
- package/src/tui/markdown.ts +0 -62
- package/src/tui/prompts.tsx +0 -205
- package/src/types.ts +0 -262
- package/src/update.ts +0 -89
- package/src/util.ts +0 -80
package/src/config/store.ts
DELETED
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
import { chmod, mkdir, readFile, stat, writeFile } from "node:fs/promises"
|
|
2
|
-
import { join } from "node:path"
|
|
3
|
-
import type { NovaAuth, NovaConfig } from "../types.ts"
|
|
4
|
-
|
|
5
|
-
const NOVA_DIR = () => join(process.env.HOME ?? "~", ".novacode")
|
|
6
|
-
const CONFIG_PATH = () => join(NOVA_DIR(), "config.json")
|
|
7
|
-
const AUTH_PATH = () => join(NOVA_DIR(), "auth.json")
|
|
8
|
-
|
|
9
|
-
const defaultConfig: NovaConfig = {
|
|
10
|
-
provider: "",
|
|
11
|
-
model: "",
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
const defaultAuth: NovaAuth = {
|
|
15
|
-
apiKeys: {},
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export async function configExists(): Promise<boolean> {
|
|
19
|
-
try {
|
|
20
|
-
await stat(CONFIG_PATH())
|
|
21
|
-
return true
|
|
22
|
-
} catch {
|
|
23
|
-
return false
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export async function loadConfig(): Promise<NovaConfig> {
|
|
28
|
-
try {
|
|
29
|
-
const raw = JSON.parse(await readFile(CONFIG_PATH(), "utf-8"))
|
|
30
|
-
return { ...defaultConfig, ...raw }
|
|
31
|
-
} catch {
|
|
32
|
-
return { ...defaultConfig }
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export async function loadAuth(): Promise<NovaAuth> {
|
|
37
|
-
try {
|
|
38
|
-
const raw = JSON.parse(await readFile(AUTH_PATH(), "utf-8"))
|
|
39
|
-
return { ...defaultAuth, ...raw }
|
|
40
|
-
} catch {
|
|
41
|
-
return { ...defaultAuth }
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
async function ensureDir(): Promise<void> {
|
|
46
|
-
await mkdir(NOVA_DIR(), { recursive: true })
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export async function saveConfig(config: NovaConfig): Promise<void> {
|
|
50
|
-
await ensureDir()
|
|
51
|
-
await writeFile(CONFIG_PATH(), JSON.stringify(config, null, 2))
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export async function saveAuth(auth: NovaAuth): Promise<void> {
|
|
55
|
-
await ensureDir()
|
|
56
|
-
await writeFile(AUTH_PATH(), JSON.stringify(auth, null, 2))
|
|
57
|
-
try {
|
|
58
|
-
await chmod(AUTH_PATH(), 0o600)
|
|
59
|
-
} catch {
|
|
60
|
-
// chmod may fail on some platforms, non-fatal
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export function getNovaDir(): string {
|
|
65
|
-
return NOVA_DIR()
|
|
66
|
-
}
|
package/src/main.ts
DELETED
|
@@ -1,205 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { parseArgs } from "node:util"
|
|
3
|
-
/**
|
|
4
|
-
* Entry point for the nova CLI.
|
|
5
|
-
* Handles configuration, CLI flags, and runs interactive TUI mode.
|
|
6
|
-
*/
|
|
7
|
-
import chalk from "chalk"
|
|
8
|
-
import { Agent } from "./agent/agent.ts"
|
|
9
|
-
import { buildSystemPrompt } from "./agent/prompt.ts"
|
|
10
|
-
import { handleSessionCommand } from "./commands/session.ts"
|
|
11
|
-
import { getProvider, MODELS } from "./config/providers.ts"
|
|
12
|
-
import { configExists, loadAuth, loadConfig } from "./config/store.ts"
|
|
13
|
-
import { runOnboarding } from "./onboarding/wizard.ts"
|
|
14
|
-
import { getSessionStore } from "./session/store.ts"
|
|
15
|
-
import { getAllTools } from "./tools/index.ts"
|
|
16
|
-
import type { Session } from "./types.ts"
|
|
17
|
-
import { getCurrentVersion, runUpdate } from "./update.ts"
|
|
18
|
-
|
|
19
|
-
function parseCli() {
|
|
20
|
-
const { values, positionals } = parseArgs({
|
|
21
|
-
options: {
|
|
22
|
-
help: { type: "boolean", short: "h" },
|
|
23
|
-
version: { type: "boolean", short: "v" },
|
|
24
|
-
provider: { type: "string" },
|
|
25
|
-
model: { type: "string" },
|
|
26
|
-
"api-key": { type: "string" },
|
|
27
|
-
session: { type: "string", short: "s" },
|
|
28
|
-
resume: { type: "boolean" },
|
|
29
|
-
n: { type: "string" },
|
|
30
|
-
limit: { type: "string" },
|
|
31
|
-
all: { type: "boolean" },
|
|
32
|
-
},
|
|
33
|
-
strict: false,
|
|
34
|
-
allowPositionals: true,
|
|
35
|
-
})
|
|
36
|
-
|
|
37
|
-
return { flags: values, args: positionals }
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function findModel(modelId: string, providerId?: string) {
|
|
41
|
-
return MODELS.find((m) => {
|
|
42
|
-
if (providerId) return m.provider === providerId && m.id === modelId
|
|
43
|
-
return m.id === modelId
|
|
44
|
-
})
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
async function main() {
|
|
48
|
-
const { flags, args } = parseCli()
|
|
49
|
-
|
|
50
|
-
if (flags.version) {
|
|
51
|
-
const version = await getCurrentVersion()
|
|
52
|
-
console.log(`nova ${version}`)
|
|
53
|
-
process.exit(0)
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
if (flags.help) {
|
|
57
|
-
console.log(`nova — open-source coding agent
|
|
58
|
-
|
|
59
|
-
Usage:
|
|
60
|
-
nova Interactive mode
|
|
61
|
-
nova update Update to latest version
|
|
62
|
-
nova --session ls List sessions (last 10 by default)
|
|
63
|
-
nova --session ls -n N List last N sessions
|
|
64
|
-
nova --session rm <id> Delete a specific session
|
|
65
|
-
nova --session rm --all Delete all sessions
|
|
66
|
-
nova --session <id> Resume a session by ID
|
|
67
|
-
nova --resume Resume the most recent session
|
|
68
|
-
|
|
69
|
-
Options:
|
|
70
|
-
-h, --help Show help
|
|
71
|
-
-v, --version Show version
|
|
72
|
-
--provider <id> Provider to use
|
|
73
|
-
--model <id> Model to use
|
|
74
|
-
--api-key <key> API key override
|
|
75
|
-
-s, --session <id> Resume/manage session`)
|
|
76
|
-
process.exit(0)
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// Handle update subcommand
|
|
80
|
-
if (args[0] === "update") {
|
|
81
|
-
await runUpdate()
|
|
82
|
-
return
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// Reject positional args — use interactive mode with / commands
|
|
86
|
-
if (args.length > 0 && !flags.session) {
|
|
87
|
-
console.error(chalk.yellow(`Unknown command: ${args.join(" ")}`))
|
|
88
|
-
console.error("Run `nova --help` for usage.")
|
|
89
|
-
process.exit(1)
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
const controller = new AbortController()
|
|
93
|
-
|
|
94
|
-
const onSignal = () => {
|
|
95
|
-
controller.abort()
|
|
96
|
-
process.stderr.write("\nAborted.\n")
|
|
97
|
-
process.exit(130)
|
|
98
|
-
}
|
|
99
|
-
process.on("SIGINT", onSignal)
|
|
100
|
-
process.on("SIGTERM", onSignal)
|
|
101
|
-
|
|
102
|
-
// First-run onboarding
|
|
103
|
-
const config = await ((await configExists()) ? loadConfig() : runOnboarding())
|
|
104
|
-
const auth = await loadAuth()
|
|
105
|
-
|
|
106
|
-
const store = await getSessionStore()
|
|
107
|
-
await store.prune()
|
|
108
|
-
|
|
109
|
-
// Handle --session commands (ls, rm)
|
|
110
|
-
if (flags.session) {
|
|
111
|
-
const sessionFlag = flags.session as string
|
|
112
|
-
if (sessionFlag === "ls" || sessionFlag === "list") {
|
|
113
|
-
const limit = parseInt((flags.n as string) || (flags.limit as string) || "10", 10)
|
|
114
|
-
await handleSessionCommand(store, ["ls"], { limit })
|
|
115
|
-
return
|
|
116
|
-
}
|
|
117
|
-
if (sessionFlag === "rm" || sessionFlag === "delete") {
|
|
118
|
-
const id = args[0]
|
|
119
|
-
const all = !!flags.all
|
|
120
|
-
await handleSessionCommand(store, ["rm", id ?? ""], { all })
|
|
121
|
-
return
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
let session: Session | null = null
|
|
126
|
-
if (flags.resume) {
|
|
127
|
-
session = await store.latest()
|
|
128
|
-
if (!session) {
|
|
129
|
-
console.error("No recent session found to resume.")
|
|
130
|
-
process.exit(1)
|
|
131
|
-
}
|
|
132
|
-
} else if (flags.session) {
|
|
133
|
-
session = await store.get(flags.session as string)
|
|
134
|
-
if (!session) {
|
|
135
|
-
console.error(`Session not found: ${flags.session}`)
|
|
136
|
-
process.exit(1)
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// CLI overrides or session default or config default
|
|
141
|
-
const providerId = (flags.provider as string) || session?.provider || config.provider
|
|
142
|
-
const modelId = (flags.model as string) || session?.model || config.model
|
|
143
|
-
const apiKey = (flags["api-key"] as string) || auth.apiKeys[providerId]
|
|
144
|
-
|
|
145
|
-
const provider = getProvider(providerId)
|
|
146
|
-
if (!provider) {
|
|
147
|
-
console.error(`Unknown provider: ${providerId}`)
|
|
148
|
-
console.error(`Available: ${getProvider("glm") ? "glm, " : ""}gemini, deepseek, openai`)
|
|
149
|
-
process.exit(1)
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
if (!apiKey) {
|
|
153
|
-
console.error(
|
|
154
|
-
`No API key for ${provider.name}. Set ${provider.envKey} or run nova for onboarding.`,
|
|
155
|
-
)
|
|
156
|
-
process.exit(1)
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
const model = findModel(modelId, providerId)
|
|
160
|
-
if (!model) {
|
|
161
|
-
console.error(`Unknown model: ${modelId}`)
|
|
162
|
-
console.error("Available models:")
|
|
163
|
-
for (const m of MODELS.filter((m) => m.provider === providerId)) {
|
|
164
|
-
console.error(` ${m.id} — ${m.name}`)
|
|
165
|
-
}
|
|
166
|
-
process.exit(1)
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
const cwd = process.cwd()
|
|
170
|
-
const tools = getAllTools(cwd)
|
|
171
|
-
const system = buildSystemPrompt(cwd, tools)
|
|
172
|
-
|
|
173
|
-
if (!session) {
|
|
174
|
-
session = await store.create(cwd, model.id, providerId)
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
const sessionId = session.id
|
|
178
|
-
const existingMessages = await store.messages(sessionId)
|
|
179
|
-
|
|
180
|
-
const agent = new Agent({
|
|
181
|
-
api: provider.api,
|
|
182
|
-
model,
|
|
183
|
-
apiKey,
|
|
184
|
-
baseUrl: provider.baseUrl,
|
|
185
|
-
system,
|
|
186
|
-
tools,
|
|
187
|
-
messages: existingMessages,
|
|
188
|
-
})
|
|
189
|
-
|
|
190
|
-
// Interactive TUI mode
|
|
191
|
-
process.off("SIGINT", onSignal)
|
|
192
|
-
process.off("SIGTERM", onSignal)
|
|
193
|
-
const { interactive } = await import("./tui/app.tsx")
|
|
194
|
-
await interactive(agent, store, sessionId)
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
process.on("unhandledRejection", (reason) => {
|
|
198
|
-
console.error("Unhandled rejection:", reason)
|
|
199
|
-
process.exit(1)
|
|
200
|
-
})
|
|
201
|
-
|
|
202
|
-
main().catch((e) => {
|
|
203
|
-
console.error("Fatal:", e)
|
|
204
|
-
process.exit(1)
|
|
205
|
-
})
|
package/src/onboarding/wizard.ts
DELETED
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import chalk from "chalk"
|
|
2
|
-
import { getModelsForProvider, getProvider, PROVIDERS } from "../config/providers.ts"
|
|
3
|
-
import { saveAuth, saveConfig } from "../config/store.ts"
|
|
4
|
-
import { standalonePassword, standaloneSelect } from "../tui/prompts.tsx"
|
|
5
|
-
import type { NovaConfig } from "../types.ts"
|
|
6
|
-
|
|
7
|
-
export async function runOnboarding(): Promise<NovaConfig> {
|
|
8
|
-
console.log(chalk.bold.cyan("\n⚡ Nova — your coding companion\n"))
|
|
9
|
-
|
|
10
|
-
const providerId = await standaloneSelect(
|
|
11
|
-
"Pick a provider",
|
|
12
|
-
PROVIDERS.map((p) => ({ value: p.id, label: p.name })),
|
|
13
|
-
)
|
|
14
|
-
if (!providerId) {
|
|
15
|
-
console.log(chalk.dim("Cancelled"))
|
|
16
|
-
process.exit(0)
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const provider = getProvider(providerId)
|
|
20
|
-
if (!provider) {
|
|
21
|
-
console.log(chalk.red("Unknown provider"))
|
|
22
|
-
process.exit(1)
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const apiKey = await standalonePassword(`Enter ${provider.name} API key`)
|
|
26
|
-
if (!apiKey) {
|
|
27
|
-
console.log(chalk.dim("Cancelled"))
|
|
28
|
-
process.exit(0)
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const models = getModelsForProvider(providerId)
|
|
32
|
-
const modelId = await standaloneSelect(
|
|
33
|
-
"Pick a default model",
|
|
34
|
-
models.map((m) => ({
|
|
35
|
-
value: m.id,
|
|
36
|
-
label: `${m.name} (${(m.contextWindow / 1000).toFixed(0)}k ctx)`,
|
|
37
|
-
})),
|
|
38
|
-
)
|
|
39
|
-
if (!modelId) {
|
|
40
|
-
console.log(chalk.dim("Cancelled"))
|
|
41
|
-
process.exit(0)
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const config: NovaConfig = {
|
|
45
|
-
provider: providerId,
|
|
46
|
-
model: modelId,
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
await saveConfig(config)
|
|
50
|
-
await saveAuth({ apiKeys: { [providerId]: apiKey } })
|
|
51
|
-
|
|
52
|
-
console.log(chalk.green("\n✓ Ready. Type your prompt or /help for commands\n"))
|
|
53
|
-
return config
|
|
54
|
-
}
|
package/src/provider/gemini.ts
DELETED
|
@@ -1,269 +0,0 @@
|
|
|
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 { EventStream } from "./stream.ts"
|
|
13
|
-
|
|
14
|
-
interface GeminiPart {
|
|
15
|
-
text?: string
|
|
16
|
-
thought?: boolean | string
|
|
17
|
-
inline_data?: { mime_type: string; data: string }
|
|
18
|
-
function_call?: { name: string; args: Record<string, unknown> }
|
|
19
|
-
function_response?: { name: string; response: Record<string, unknown> }
|
|
20
|
-
thought_signature?: string
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
interface GeminiContent {
|
|
24
|
-
role: "user" | "model"
|
|
25
|
-
parts: GeminiPart[]
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Maps our internal Msg format to the Gemini 'Content' format.
|
|
30
|
-
* Groups consecutive tool_result messages into a single Gemini message.
|
|
31
|
-
*/
|
|
32
|
-
function msgsToGemini(messages: Msg[]): GeminiContent[] {
|
|
33
|
-
const contents: GeminiContent[] = []
|
|
34
|
-
|
|
35
|
-
for (const msg of messages) {
|
|
36
|
-
if (msg.role === "user") {
|
|
37
|
-
const parts: GeminiPart[] =
|
|
38
|
-
typeof msg.content === "string"
|
|
39
|
-
? [{ text: msg.content }]
|
|
40
|
-
: msg.content.map((c) => {
|
|
41
|
-
if (c.type === "text") return { text: c.text }
|
|
42
|
-
if (c.type === "image") return { inline_data: { mime_type: c.mime, data: c.data } }
|
|
43
|
-
return { text: "" }
|
|
44
|
-
})
|
|
45
|
-
contents.push({ role: "user", parts })
|
|
46
|
-
} else if (msg.role === "assistant") {
|
|
47
|
-
const parts: GeminiPart[] = msg.content.map((c) => {
|
|
48
|
-
if (c.type === "text") return { text: c.text, thought_signature: c.signature }
|
|
49
|
-
if (c.type === "thinking")
|
|
50
|
-
return { thought: true, text: c.text, thought_signature: c.signature }
|
|
51
|
-
if (c.type === "tool_call")
|
|
52
|
-
return { function_call: { name: c.name, args: c.args }, thought_signature: c.signature }
|
|
53
|
-
return { text: "" }
|
|
54
|
-
})
|
|
55
|
-
contents.push({ role: "model", parts })
|
|
56
|
-
} else if (msg.role === "tool_result") {
|
|
57
|
-
const part: GeminiPart = {
|
|
58
|
-
function_response: {
|
|
59
|
-
name: msg.tool,
|
|
60
|
-
response: {
|
|
61
|
-
content: msg.content
|
|
62
|
-
.map((c) => (c.type === "text" ? c.text : JSON.stringify(c)))
|
|
63
|
-
.join("\n"),
|
|
64
|
-
},
|
|
65
|
-
},
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
const last = contents[contents.length - 1]
|
|
69
|
-
// Gemini requires alternating roles; multiple function_responses group into one 'user' message.
|
|
70
|
-
if (last && last.role === "user" && last.parts.some((p) => p.function_response)) {
|
|
71
|
-
last.parts.push(part)
|
|
72
|
-
} else {
|
|
73
|
-
contents.push({ role: "user", parts: [part] })
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
return contents
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function toolsToGemini(tools: ToolDef[]): unknown[] {
|
|
82
|
-
if (tools.length === 0) return []
|
|
83
|
-
return [
|
|
84
|
-
{
|
|
85
|
-
function_declarations: tools.map((t) => ({
|
|
86
|
-
name: t.name,
|
|
87
|
-
description: t.description,
|
|
88
|
-
parameters: t.parameters,
|
|
89
|
-
})),
|
|
90
|
-
},
|
|
91
|
-
]
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
export const streamGemini: StreamFn = (
|
|
95
|
-
opts: StreamOpts,
|
|
96
|
-
): EventStream<StreamEvent, AssistantResult> => {
|
|
97
|
-
const es = new EventStream<StreamEvent, AssistantResult>()
|
|
98
|
-
|
|
99
|
-
;(async () => {
|
|
100
|
-
let usage: Usage = { in: 0, out: 0 }
|
|
101
|
-
const content: ContentPart[] = []
|
|
102
|
-
|
|
103
|
-
try {
|
|
104
|
-
const baseUrl = opts.baseUrl || "https://generativelanguage.googleapis.com"
|
|
105
|
-
const url = `${baseUrl}/v1beta/models/${opts.model.id}:streamGenerateContent?alt=sse&key=${opts.apiKey}`
|
|
106
|
-
|
|
107
|
-
const body = {
|
|
108
|
-
contents: msgsToGemini(opts.messages),
|
|
109
|
-
system_instruction: opts.system ? { parts: [{ text: opts.system }] } : undefined,
|
|
110
|
-
tools: opts.tools.length > 0 ? toolsToGemini(opts.tools) : undefined,
|
|
111
|
-
generationConfig: {
|
|
112
|
-
thinkingConfig: opts.model.supportsThinking ? { thinkingLevel: "low" } : undefined,
|
|
113
|
-
},
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
const response = await fetch(url, {
|
|
117
|
-
method: "POST",
|
|
118
|
-
headers: {
|
|
119
|
-
"Content-Type": "application/json",
|
|
120
|
-
"Api-Revision": "2026-05-20",
|
|
121
|
-
},
|
|
122
|
-
body: JSON.stringify(body),
|
|
123
|
-
signal: opts.signal,
|
|
124
|
-
})
|
|
125
|
-
|
|
126
|
-
if (!response.ok) {
|
|
127
|
-
const text = await response.text()
|
|
128
|
-
let msg = text
|
|
129
|
-
try {
|
|
130
|
-
const json = JSON.parse(text)
|
|
131
|
-
msg = json.error?.message || json.message || text
|
|
132
|
-
} catch {
|
|
133
|
-
/* use raw text */
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
const errorMsg = `Gemini Error (${response.status}): ${msg}`
|
|
137
|
-
es.push({ type: "text_delta", text: errorMsg })
|
|
138
|
-
es.finish({
|
|
139
|
-
content: [{ type: "text", text: errorMsg }],
|
|
140
|
-
usage: { in: 0, out: 0 },
|
|
141
|
-
stop: "error",
|
|
142
|
-
})
|
|
143
|
-
return
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
const reader = response.body?.getReader()
|
|
147
|
-
if (!reader) {
|
|
148
|
-
es.finish({ content: [], usage: { in: 0, out: 0 }, stop: "error" })
|
|
149
|
-
return
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
const decoder = new TextDecoder()
|
|
153
|
-
let buffer = ""
|
|
154
|
-
let stop: StopReason = "stop"
|
|
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
|
-
const last = content[content.length - 1]
|
|
204
|
-
if (last?.type === "thinking") {
|
|
205
|
-
last.text += thoughtText
|
|
206
|
-
} else {
|
|
207
|
-
content.push({ type: "thinking", text: thoughtText, signature: sig })
|
|
208
|
-
}
|
|
209
|
-
} else {
|
|
210
|
-
es.push({ type: "text_delta", text: part.text })
|
|
211
|
-
const last = content[content.length - 1]
|
|
212
|
-
if (last?.type === "text") {
|
|
213
|
-
last.text += part.text
|
|
214
|
-
} else {
|
|
215
|
-
content.push({ type: "text", text: part.text, signature: sig })
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
// Handle function calls (can be snake_case or camelCase in some API versions)
|
|
221
|
-
const fc = part.functionCall || part.function_call
|
|
222
|
-
if (fc) {
|
|
223
|
-
const name = fc.name
|
|
224
|
-
const args = (fc.args as Record<string, unknown>) || {}
|
|
225
|
-
const id = `call_${Math.random().toString(36).slice(2, 9)}`
|
|
226
|
-
|
|
227
|
-
const toolCall: ContentPart = {
|
|
228
|
-
type: "tool_call",
|
|
229
|
-
id,
|
|
230
|
-
name,
|
|
231
|
-
args,
|
|
232
|
-
signature: sig,
|
|
233
|
-
}
|
|
234
|
-
content.push(toolCall)
|
|
235
|
-
es.push({ type: "tool_call", call: toolCall })
|
|
236
|
-
stop = "tool_use"
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
} catch (_e) {
|
|
241
|
-
if (data.trim() !== "" && data.trim() !== "[DONE]") {
|
|
242
|
-
// skip noise
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
es.finish({ content, usage, stop })
|
|
249
|
-
} catch (e) {
|
|
250
|
-
if (opts.signal?.aborted) {
|
|
251
|
-
es.finish({
|
|
252
|
-
content,
|
|
253
|
-
usage,
|
|
254
|
-
stop: "aborted",
|
|
255
|
-
})
|
|
256
|
-
return
|
|
257
|
-
}
|
|
258
|
-
const errorMsg = `Gemini Network/Request Error: ${e instanceof Error ? e.message : String(e)}`
|
|
259
|
-
es.push({ type: "text_delta", text: errorMsg })
|
|
260
|
-
es.finish({
|
|
261
|
-
content: [{ type: "text", text: errorMsg }],
|
|
262
|
-
usage: { in: 0, out: 0 },
|
|
263
|
-
stop: "error",
|
|
264
|
-
})
|
|
265
|
-
}
|
|
266
|
-
})()
|
|
267
|
-
|
|
268
|
-
return es
|
|
269
|
-
}
|