Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 23 additions & 5 deletions src/main/agent/runtime.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { createDeepAgent } from "deepagents"
import { getDefaultModel } from "../ipc/models"
import { getDefaultModel, getModelConfig } from "../ipc/models"
import { getApiKey, getThreadCheckpointPath } from "../storage"
import { ChatAnthropic } from "@langchain/anthropic"
import { ChatOpenAI } from "@langchain/openai"
Expand Down Expand Up @@ -62,11 +62,14 @@ export async function closeCheckpointer(threadId: string): Promise<void> {
function getModelInstance(
modelId?: string
): ChatAnthropic | ChatOpenAI | ChatGoogleGenerativeAI | string {
const model = modelId || getDefaultModel()
console.log("[Runtime] Using model:", model)
const selectedModelId = modelId || getDefaultModel()
const modelConfig = getModelConfig(selectedModelId)
const provider = modelConfig?.provider
const model = modelConfig?.model ?? selectedModelId
console.log("[Runtime] Using model:", selectedModelId)

// Determine provider from model ID
if (model.startsWith("claude")) {
if (provider === "anthropic" || model.startsWith("claude")) {
const apiKey = getApiKey("anthropic")
console.log("[Runtime] Anthropic API key present:", !!apiKey)
if (!apiKey) {
Expand All @@ -77,6 +80,7 @@ function getModelInstance(
anthropicApiKey: apiKey
})
} else if (
provider === "openai" ||
model.startsWith("gpt") ||
model.startsWith("o1") ||
model.startsWith("o3") ||
Expand All @@ -91,7 +95,7 @@ function getModelInstance(
model,
openAIApiKey: apiKey
})
} else if (model.startsWith("gemini")) {
} else if (provider === "google" || model.startsWith("gemini")) {
const apiKey = getApiKey("google")
console.log("[Runtime] Google API key present:", !!apiKey)
if (!apiKey) {
Expand All @@ -101,6 +105,20 @@ function getModelInstance(
model,
apiKey: apiKey
})
} else if (provider === "volcengine" || model.startsWith("ep-")) {
const apiKey = getApiKey("volcengine")
console.log("[Runtime] Volcengine API key present:", !!apiKey)
if (!apiKey) {
throw new Error("Volcengine API key not configured")
}
return new ChatOpenAI({
model,
apiKey,
useResponsesApi: true,
configuration: {
baseURL: "https://ark.cn-beijing.volces.com/api/v3"
}
})
}

// Default to model string (let deepagents handle it)
Expand Down
73 changes: 70 additions & 3 deletions src/main/ipc/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import Store from "electron-store"
import * as fs from "fs/promises"
import * as path from "path"
import type {
AddCustomModelParams,
ModelConfig,
Provider,
SetApiKeyParams,
Expand All @@ -19,11 +20,17 @@ const store = new Store({
cwd: getOpenworkDir()
})

type StoredCustomModel = Omit<ModelConfig, "available" | "custom">

const CUSTOM_MODELS_KEY = "customModels"
const DEFAULT_MODEL_ID = "claude-sonnet-4-5-20250929"

// Provider configurations
const PROVIDERS: Omit<Provider, "hasApiKey">[] = [
{ id: "anthropic", name: "Anthropic" },
{ id: "openai", name: "OpenAI" },
{ id: "google", name: "Google" }
{ id: "google", name: "Google" },
{ id: "volcengine", name: "Volcengine" }
]

// Available models configuration (updated Jan 2026)
Expand Down Expand Up @@ -204,11 +211,29 @@ const AVAILABLE_MODELS: ModelConfig[] = [
}
]

function getStoredCustomModels(): StoredCustomModel[] {
return store.get(CUSTOM_MODELS_KEY, []) as StoredCustomModel[]
}

function setStoredCustomModels(models: StoredCustomModel[]): void {
store.set(CUSTOM_MODELS_KEY, models)
}

function getAllModels(): ModelConfig[] {
const baseModels = AVAILABLE_MODELS.map((model) => ({ ...model, custom: false }))
const customModels = getStoredCustomModels().map((model) => ({
...model,
available: true,
custom: true
}))
return [...baseModels, ...customModels]
}

export function registerModelHandlers(ipcMain: IpcMain): void {
// List available models
ipcMain.handle("models:list", async () => {
// Check which models have API keys configured
return AVAILABLE_MODELS.map((model) => ({
return getAllModels().map((model) => ({
...model,
available: hasApiKey(model.provider)
}))
Expand All @@ -224,6 +249,44 @@ export function registerModelHandlers(ipcMain: IpcMain): void {
store.set("defaultModel", modelId)
})

// Add custom model (e.g., Volcengine Ark endpoint)
ipcMain.handle("models:addCustom", async (_event, params: AddCustomModelParams) => {
const id = params.id.trim()
if (!id) {
throw new Error("Model ID is required")
}

if (params.provider !== "volcengine") {
throw new Error("Custom models are only supported for Volcengine")
}

const existing = new Set(getAllModels().map((model) => model.id))
if (existing.has(id)) {
throw new Error(`Model ID already exists: ${id}`)
}

const model = (params.model || id).trim()
const name = (params.name || id).trim()
const description = params.description?.trim() || "Custom Volcengine Ark model"

const next = [
...getStoredCustomModels(),
{ id, provider: params.provider, model, name, description }
]
setStoredCustomModels(next)
})

// Delete custom model
ipcMain.handle("models:deleteCustom", async (_event, modelId: string) => {
const next = getStoredCustomModels().filter((model) => model.id !== modelId)
setStoredCustomModels(next)

const defaultModel = store.get("defaultModel", DEFAULT_MODEL_ID) as string
if (defaultModel === modelId) {
store.set("defaultModel", DEFAULT_MODEL_ID)
}
})

// Set API key for a provider (stored in ~/.openwork/.env)
ipcMain.handle("models:setApiKey", async (_event, { provider, apiKey }: SetApiKeyParams) => {
setApiKey(provider, apiKey)
Expand Down Expand Up @@ -519,5 +582,9 @@ export function registerModelHandlers(ipcMain: IpcMain): void {
export { getApiKey } from "../storage"

export function getDefaultModel(): string {
return store.get("defaultModel", "claude-sonnet-4-5-20250929") as string
return store.get("defaultModel", DEFAULT_MODEL_ID) as string
}

export function getModelConfig(modelId: string): ModelConfig | undefined {
return getAllModels().find((model) => model.id === modelId)
}
3 changes: 2 additions & 1 deletion src/main/storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ const ENV_VAR_NAMES: Record<ProviderId, string> = {
anthropic: "ANTHROPIC_API_KEY",
openai: "OPENAI_API_KEY",
google: "GOOGLE_API_KEY",
ollama: "" // Ollama doesn't require an API key
ollama: "", // Ollama doesn't require an API key
volcengine: "ARK_API_KEY"
}

export function getOpenworkDir(): string {
Expand Down
11 changes: 10 additions & 1 deletion src/main/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,14 +80,22 @@ export interface Run {
}

// Provider configuration
export type ProviderId = "anthropic" | "openai" | "google" | "ollama"
export type ProviderId = "anthropic" | "openai" | "google" | "ollama" | "volcengine"

export interface Provider {
id: ProviderId
name: string
hasApiKey: boolean
}

export interface AddCustomModelParams {
id: string
provider: ProviderId
model?: string
name?: string
description?: string
}

// Model configuration
export interface ModelConfig {
id: string
Expand All @@ -96,6 +104,7 @@ export interface ModelConfig {
model: string
description?: string
available: boolean
custom?: boolean
}

// Subagent types (from deepagentsjs)
Expand Down
8 changes: 8 additions & 0 deletions src/preload/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,14 @@ interface CustomAPI {
setDefault: (modelId: string) => Promise<void>
setApiKey: (provider: string, apiKey: string) => Promise<void>
getApiKey: (provider: string) => Promise<string | null>
addCustomModel: (params: {
id: string
provider: string
model?: string
name?: string
description?: string
}) => Promise<void>
deleteCustomModel: (modelId: string) => Promise<void>
}
workspace: {
get: (threadId?: string) => Promise<string | null>
Expand Down
12 changes: 12 additions & 0 deletions src/preload/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,18 @@ const api = {
getApiKey: (provider: string): Promise<string | null> => {
return ipcRenderer.invoke("models:getApiKey", provider)
},
addCustomModel: (params: {
id: string
provider: string
model?: string
name?: string
description?: string
}): Promise<void> => {
return ipcRenderer.invoke("models:addCustom", params)
},
deleteCustomModel: (modelId: string): Promise<void> => {
return ipcRenderer.invoke("models:deleteCustom", modelId)
},
deleteApiKey: (provider: string): Promise<void> => {
return ipcRenderer.invoke("models:deleteApiKey", provider)
}
Expand Down
3 changes: 2 additions & 1 deletion src/renderer/src/components/chat/ApiKeyDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ interface ApiKeyDialogProps {
const PROVIDER_INFO: Record<string, { placeholder: string; envVar: string }> = {
anthropic: { placeholder: "sk-ant-...", envVar: "ANTHROPIC_API_KEY" },
openai: { placeholder: "sk-...", envVar: "OPENAI_API_KEY" },
google: { placeholder: "AIza...", envVar: "GOOGLE_API_KEY" }
google: { placeholder: "AIza...", envVar: "GOOGLE_API_KEY" },
volcengine: { placeholder: "ark-...", envVar: "ARK_API_KEY" }
}

export function ApiKeyDialog({
Expand Down
Loading