mirror of
https://github.com/aljazceru/opencode.git
synced 2025-12-24 11:14:23 +01:00
sync
This commit is contained in:
@@ -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",
|
||||
},
|
||||
})
|
||||
|
||||
@@ -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(),
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
53
packages/opencode/src/util/error.ts
Normal file
53
packages/opencode/src/util/error.ts
Normal 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(),
|
||||
}),
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user