This commit is contained in:
Dax Raad
2025-06-09 14:01:11 -04:00
parent fa3253d1b6
commit 021fd3fcb5
8 changed files with 734 additions and 198 deletions

View File

@@ -38,7 +38,9 @@ export namespace LSPServer {
? path.resolve(process.cwd(), process.argv0)
: process.argv0
return spawn(root, ["x", "typescript-language-server", "--stdio"], {
argv0: "bun",
env: {
...process.env,
BUN_BE_BUN: "1",
},
})

View File

@@ -23,6 +23,7 @@ import { WriteTool } from "../tool/write"
import { TodoReadTool, TodoWriteTool } from "../tool/todo"
import { AuthAnthropic } from "../auth/anthropic"
import { ModelsDev } from "./models"
import { NamedError } from "../util/error"
export namespace Provider {
const log = Log.create({ service: "provider" })
@@ -75,9 +76,18 @@ export namespace Provider {
string,
(provider: Info) => Promise<Record<string, any> | false>
> = {
anthropic: async () => {
async anthropic(provider) {
const access = await AuthAnthropic.access()
if (access)
if (access) {
// claude sub doesn't have usage cost
for (const model of Object.values(provider.models)) {
model.cost = {
input: 0,
inputCached: 0,
output: 0,
outputCached: 0,
}
}
return {
apiKey: "",
headers: {
@@ -85,16 +95,15 @@ export namespace Provider {
"anthropic-beta": "oauth-2025-04-20",
},
}
return env("ANTHROPIC_API_KEY")
}
return env("ANTHROPIC_API_KEY")(provider)
},
google: env("GOOGLE_GENERATIVE_AI_API_KEY"),
openai: env("OPENAI_API_KEY"),
}
const state = App.state("provider", async () => {
log.info("loading config")
const config = await Config.get()
log.info("loading providers")
const database: Record<string, Provider.Info> = await ModelsDev.get()
const providers: {
@@ -134,6 +143,10 @@ export namespace Provider {
}
}
for (const providerID of Object.keys(providers)) {
log.info("loaded", { providerID })
}
return {
models,
providers,
@@ -148,28 +161,32 @@ export namespace Provider {
}
async function getSDK(providerID: string) {
const s = await state()
if (s.sdk.has(providerID)) return s.sdk.get(providerID)!
const dir = path.join(
Global.Path.cache,
`node_modules`,
`@ai-sdk`,
providerID,
)
if (!(await Bun.file(path.join(dir, "package.json")).exists())) {
log.info("installing", {
return (async () => {
const s = await state()
const existing = s.sdk.get(providerID)
if (existing) return existing
const dir = path.join(
Global.Path.cache,
`node_modules`,
`@ai-sdk`,
providerID,
})
await BunProc.run(["add", `@ai-sdk/${providerID}@alpha`], {
cwd: Global.Path.cache,
})
}
const mod = await import(path.join(dir))
const fn = mod[Object.keys(mod).find((key) => key.startsWith("create"))!]
const loaded = fn(s.providers[providerID]?.options)
s.sdk.set(providerID, loaded)
return loaded as SDK
)
if (!(await Bun.file(path.join(dir, "package.json")).exists())) {
log.info("installing", {
providerID,
})
await BunProc.run(["add", `@ai-sdk/${providerID}@alpha`], {
cwd: Global.Path.cache,
})
}
const mod = await import(path.join(dir))
const fn = mod[Object.keys(mod).find((key) => key.startsWith("create"))!]
const loaded = fn(s.providers[providerID]?.options)
s.sdk.set(providerID, loaded)
return loaded as SDK
})().catch((e) => {
throw new InitError({ providerID: providerID }, { cause: e })
})
}
export async function getModel(providerID: string, modelID: string) {
@@ -183,12 +200,11 @@ export namespace Provider {
})
const provider = s.providers[providerID]
if (!provider) throw new ModelNotFoundError(modelID)
if (!provider) throw new ModelNotFoundError({ providerID, modelID })
const info = provider.info.models[modelID]
if (!info) throw new ModelNotFoundError(modelID)
if (!info) throw new ModelNotFoundError({ providerID, modelID })
const sdk = await getSDK(providerID)
if (!sdk) throw new ModelNotFoundError(modelID)
try {
const language = sdk.languageModel(modelID)
@@ -202,7 +218,14 @@ export namespace Provider {
language,
}
} catch (e) {
if (e instanceof NoSuchModelError) throw new ModelNotFoundError(modelID)
if (e instanceof NoSuchModelError)
throw new ModelNotFoundError(
{
modelID: modelID,
providerID,
},
{ cause: e },
)
throw e
}
}
@@ -259,9 +282,26 @@ export namespace Provider {
return TOOL_MAPPING[providerID] ?? TOOLS
}
class ModelNotFoundError extends Error {
constructor(public readonly model: string) {
super()
}
}
export const ModelNotFoundError = NamedError.create(
"ProviderModelNotFoundError",
z.object({
providerID: z.string(),
modelID: z.string(),
}),
)
export const InitError = NamedError.create(
"ProviderInitError",
z.object({
providerID: z.string(),
}),
)
export const AuthError = NamedError.create(
"ProviderAuthError",
z.object({
providerID: z.string(),
message: z.string(),
}),
)
}

View File

@@ -11,6 +11,26 @@ import { Provider } from "../provider/provider"
import { App } from "../app/app"
import { Global } from "../global"
import { mapValues } from "remeda"
import { NamedError } from "../util/error"
const ERRORS = {
400: {
description: "Bad request",
content: {
"application/json": {
schema: resolver(
z
.object({
data: z.record(z.string(), z.any()),
})
.openapi({
ref: "Error",
}),
),
},
},
},
} as const
export namespace Server {
const log = Log.create({ service: "server" })
@@ -22,13 +42,15 @@ export namespace Server {
const result = app
.onError((err, c) => {
log.error("error", err)
if (err instanceof NamedError) {
return c.json(err.toObject(), {
status: 400,
})
}
return c.json(
new NamedError.Unknown({ message: err.toString() }).toObject(),
{
error: err.toString(),
},
{
status: 500,
status: 400,
},
)
})
@@ -197,6 +219,7 @@ export namespace Server {
describeRoute({
description: "Create a new session",
responses: {
...ERRORS,
200: {
description: "Successfully created session",
content: {

View File

@@ -6,6 +6,7 @@ import { Log } from "../util/log"
import {
convertToModelMessages,
generateText,
LoadAPIKeyError,
stepCountIs,
streamText,
tool,
@@ -28,6 +29,7 @@ import { Provider } from "../provider/provider"
import { SessionContext } from "./context"
import { ListTool } from "../tool/ls"
import { MCP } from "../mcp"
import { NamedError } from "../util/error"
export namespace Session {
const log = Log.create({ service: "session" })
@@ -59,6 +61,12 @@ export namespace Session {
info: Info,
}),
),
Error: Bus.event(
"session.error",
z.object({
error: Message.Info.shape.metadata.shape.error,
}),
),
}
const state = App.state("session", () => {
@@ -296,11 +304,13 @@ export namespace Session {
},
]),
model: model.language,
}).then((result) => {
return Session.update(input.sessionID, (draft) => {
draft.title = result.text
})
})
.then((result) => {
return Session.update(input.sessionID, (draft) => {
draft.title = result.text
})
})
.catch(() => {})
await updateMessage(system)
}
const msg: Message.Info = {
@@ -506,11 +516,27 @@ export namespace Session {
assistant.cost = usage.cost
await updateMessage(next)
},
onError(input) {
log.error("error", input)
if (input.error instanceof Error) {
next.metadata.error = input.error.toString()
onError(err) {
log.error("error", err)
switch (true) {
case LoadAPIKeyError.isInstance(err.error):
next.metadata.error = new Provider.AuthError(
{
providerID: input.providerID,
message: err.error.message,
},
{ cause: err.error },
).toObject()
break
case err.error instanceof Error:
next.metadata.error = new NamedError.Unknown(
{ message: err.error.toString() },
{ cause: err.error },
).toObject()
}
Bus.publish(Event.Error, {
error: next.metadata.error,
})
},
async prepareStep(step) {
next.parts.push({
@@ -532,7 +558,7 @@ export namespace Session {
})
await result.consumeStream({
onError: (err) => {
log.error("error", {
log.error("stream error", {
err,
})
},

View File

@@ -1,5 +1,7 @@
import z from "zod"
import { Bus } from "../bus"
import { Provider } from "../provider/provider"
import { NamedError } from "../util/error"
export namespace Message {
export const ToolCall = z
@@ -138,7 +140,10 @@ export namespace Message {
created: z.number(),
completed: z.number().optional(),
}),
error: z.string().optional(),
error: z.discriminatedUnion("name", [
Provider.AuthError.Schema,
NamedError.Unknown.Schema,
]),
sessionID: z.string(),
tool: z.record(z.string(), z.any()),
assistant: z

View File

@@ -0,0 +1,53 @@
import { z, type ZodSchema } from "zod"
export abstract class NamedError extends Error {
abstract schema(): ZodSchema
abstract toObject(): { name: string; data: any }
static create<Name extends string, Data extends ZodSchema>(
name: Name,
data: Data,
) {
const result = class extends NamedError {
public static readonly Schema = z
.object({
name: z.literal(name),
data: data,
})
.openapi({
ref: name,
})
constructor(
public readonly data: z.input<Data>,
options?: ErrorOptions,
) {
super(name, options)
this.name = name
}
static isInstance(input: any): input is InstanceType<typeof result> {
return "name" in input && input.name === name
}
schema() {
return data
}
toObject() {
return {
name: name,
data: this.data,
}
}
}
return result
}
public static readonly Unknown = NamedError.create(
"UnknownError",
z.object({
message: z.string(),
}),
)
}