Skip to content
Open
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
81 changes: 81 additions & 0 deletions docs/PROXY_SETUP.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# Proxy/Gateway Configuration

OpenWork now supports custom base URLs for API providers, allowing you to route requests through internal gateways or proxy servers.

## Configuration

Base URLs are stored in the `~/.openwork/.env` file alongside API keys.

### Setting Base URLs via Environment Variables

You can configure custom base URLs by setting environment variables:

```bash
# Anthropic custom base URL
ANTHROPIC_BASE_URL=https://your-proxy.example.com/anthropic

# OpenAI custom base URL
OPENAI_BASE_URL=https://your-proxy.example.com/openai

# Google custom base URL
GOOGLE_BASE_URL=https://your-proxy.example.com/google
```

### Setting Base URLs via API

You can also configure base URLs programmatically through the IPC API:

```typescript
// Set a custom base URL
await window.api.models.setBaseUrl('anthropic', 'https://your-proxy.example.com/anthropic')

// Get the current base URL
const baseUrl = await window.api.models.getBaseUrl('anthropic')

// Remove a custom base URL (revert to default)
await window.api.models.deleteBaseUrl('anthropic')
```

## How It Works

When a base URL is configured for a provider:

1. **Anthropic (Claude)**: Uses `clientOptions.baseURL`
2. **OpenAI (GPT)**: Uses `configuration.baseURL`
3. **Google (Gemini)**: Uses `baseUrl` configuration option

The application will automatically use the custom base URL when making API calls, allowing you to:

- Route requests through internal API gateways
- Use proxy servers for compliance/security
- Implement custom rate limiting or caching layers
- Test against local/staging endpoints

## Example Use Case

If you have an internal API gateway at `https://ai-gateway.company.com`:

```bash
# In ~/.openwork/.env
ANTHROPIC_API_KEY=sk-ant-your-key
ANTHROPIC_BASE_URL=https://ai-gateway.company.com/anthropic

OPENAI_API_KEY=sk-your-openai-key
OPENAI_BASE_URL=https://ai-gateway.company.com/openai
```

All API requests will now be routed through your gateway while still using the official provider SDKs.

## Storage Location

Base URLs and API keys are stored in:
- **Location**: `~/.openwork/.env`
- **Format**: Standard environment variable format
- **Security**: File-based storage (ensure proper file permissions)

## Notes

- Base URLs are optional - if not set, requests go directly to provider APIs
- Custom base URLs can be set per-provider independently
- Changes take effect immediately for new agent sessions
- The `.env` file is created automatically in the `.openwork` directory in your home folder
51 changes: 43 additions & 8 deletions src/main/agent/runtime.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { createDeepAgent } from "deepagents"
import { getDefaultModel } from "../ipc/models"
import { getApiKey, getThreadCheckpointPath } from "../storage"
import { getApiKey, getThreadCheckpointPath, getBaseUrl } from "../storage"
import { ChatAnthropic } from "@langchain/anthropic"
import { ChatOpenAI } from "@langchain/openai"
import { ChatGoogleGenerativeAI } from "@langchain/google-genai"
Expand Down Expand Up @@ -60,7 +60,8 @@ export async function closeCheckpointer(threadId: string): Promise<void> {

// Get the appropriate model instance based on configuration
function getModelInstance(
modelId?: string
modelId?: string,
customConfig?: { base_url?: string }
): ChatAnthropic | ChatOpenAI | ChatGoogleGenerativeAI | string {
const model = modelId || getDefaultModel()
console.log("[Runtime] Using model:", model)
Expand All @@ -72,10 +73,22 @@ function getModelInstance(
if (!apiKey) {
throw new Error("Anthropic API key not configured")
}
return new ChatAnthropic({

const config: any = {
model,
anthropicApiKey: apiKey
})
}

// Add base URL if provided in customConfig or from storage
const baseUrl = customConfig?.base_url || getBaseUrl("anthropic")
if (baseUrl) {
console.log("[Runtime] Using custom base URL for Anthropic:", baseUrl)
config.clientOptions = {
baseURL: baseUrl
}
}

return new ChatAnthropic(config)
} else if (
model.startsWith("gpt") ||
model.startsWith("o1") ||
Expand All @@ -87,20 +100,42 @@ function getModelInstance(
if (!apiKey) {
throw new Error("OpenAI API key not configured")
}
return new ChatOpenAI({

const config: any = {
model,
openAIApiKey: apiKey
})
}

// Add base URL if provided in customConfig or from storage
const baseUrl = customConfig?.base_url || getBaseUrl("openai")
if (baseUrl) {
console.log("[Runtime] Using custom base URL for OpenAI:", baseUrl)
config.configuration = {
baseURL: baseUrl
}
}

return new ChatOpenAI(config)
} else if (model.startsWith("gemini")) {
const apiKey = getApiKey("google")
console.log("[Runtime] Google API key present:", !!apiKey)
if (!apiKey) {
throw new Error("Google API key not configured")
}
return new ChatGoogleGenerativeAI({

const config: any = {
model,
apiKey: apiKey
})
}

// Add base URL if provided in customConfig or from storage
const baseUrl = customConfig?.base_url || getBaseUrl("google")
if (baseUrl) {
console.log("[Runtime] Using custom base URL for Google:", baseUrl)
config.baseUrl = baseUrl
}

return new ChatGoogleGenerativeAI(config)
}

// Default to model string (let deepagents handle it)
Expand Down
18 changes: 17 additions & 1 deletion src/main/ipc/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@ import type {
ModelConfig,
Provider,
SetApiKeyParams,
SetBaseUrlParams,
WorkspaceSetParams,
WorkspaceLoadParams,
WorkspaceFileParams
} from "../types"
import { startWatching, stopWatching } from "../services/workspace-watcher"
import { getOpenworkDir, getApiKey, setApiKey, deleteApiKey, hasApiKey } from "../storage"
import { getOpenworkDir, getApiKey, setApiKey, deleteApiKey, hasApiKey, getBaseUrl, setBaseUrl, deleteBaseUrl } from "../storage"

// Store for non-sensitive settings only (no encryption needed)
const store = new Store({
Expand Down Expand Up @@ -239,6 +240,21 @@ export function registerModelHandlers(ipcMain: IpcMain): void {
deleteApiKey(provider)
})

// Set base URL for a provider (stored in ~/.openwork/.env)
ipcMain.handle("models:setBaseUrl", async (_event, { provider, baseUrl }: SetBaseUrlParams) => {
setBaseUrl(provider, baseUrl)
})

// Get base URL for a provider (from ~/.openwork/.env or process.env)
ipcMain.handle("models:getBaseUrl", async (_event, provider: string) => {
return getBaseUrl(provider) ?? null
})

// Delete base URL for a provider
ipcMain.handle("models:deleteBaseUrl", async (_event, provider: string) => {
deleteBaseUrl(provider)
})

// List providers with their API key status
ipcMain.handle("models:listProviders", async () => {
return PROVIDERS.map((provider) => ({
Expand Down
37 changes: 37 additions & 0 deletions src/main/storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,3 +122,40 @@ export function deleteApiKey(provider: string): void {
export function hasApiKey(provider: string): boolean {
return !!getApiKey(provider)
}

// Base URL management for proxy/gateway support
export function getBaseUrl(provider: string): string | undefined {
const envVarName = `${ENV_VAR_NAMES[provider]?.replace('_API_KEY', '')}_BASE_URL`
if (!envVarName) return undefined

// Check .env file first
const env = parseEnvFile()
if (env[envVarName]) return env[envVarName]

// Fall back to process environment
return process.env[envVarName]
}

export function setBaseUrl(provider: string, baseUrl: string): void {
const envVarName = `${ENV_VAR_NAMES[provider]?.replace('_API_KEY', '')}_BASE_URL`
if (!envVarName) return

const env = parseEnvFile()
env[envVarName] = baseUrl
writeEnvFile(env)

// Also set in process.env for current session
process.env[envVarName] = baseUrl
}

export function deleteBaseUrl(provider: string): void {
const envVarName = `${ENV_VAR_NAMES[provider]?.replace('_API_KEY', '')}_BASE_URL`
if (!envVarName) return

const env = parseEnvFile()
delete env[envVarName]
writeEnvFile(env)

// Also clear from process.env
delete process.env[envVarName]
}
7 changes: 7 additions & 0 deletions src/main/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@ export interface SetApiKeyParams {
apiKey: string
}

export interface SetBaseUrlParams {
provider: string
baseUrl: string
}

// =============================================================================

export interface Thread {
Expand Down Expand Up @@ -96,6 +101,8 @@ export interface ModelConfig {
model: string
description?: string
available: boolean
base_url?: string // Custom base URL for proxy/gateway support
custom?: boolean // Flag to identify custom models
}

// Subagent types (from deepagentsjs)
Expand Down
3 changes: 3 additions & 0 deletions src/preload/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ interface CustomAPI {
setDefault: (modelId: string) => Promise<void>
setApiKey: (provider: string, apiKey: string) => Promise<void>
getApiKey: (provider: string) => Promise<string | null>
setBaseUrl: (provider: string, baseUrl: string) => Promise<void>
getBaseUrl: (provider: string) => Promise<string | null>
deleteBaseUrl: (provider: string) => Promise<void>
}
workspace: {
get: (threadId?: string) => Promise<string | null>
Expand Down
9 changes: 9 additions & 0 deletions src/preload/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,15 @@ const api = {
},
deleteApiKey: (provider: string): Promise<void> => {
return ipcRenderer.invoke("models:deleteApiKey", provider)
},
setBaseUrl: (provider: string, baseUrl: string): Promise<void> => {
return ipcRenderer.invoke("models:setBaseUrl", { provider, baseUrl })
},
getBaseUrl: (provider: string): Promise<string | null> => {
return ipcRenderer.invoke("models:getBaseUrl", provider)
},
deleteBaseUrl: (provider: string): Promise<void> => {
return ipcRenderer.invoke("models:deleteBaseUrl", provider)
}
},
workspace: {
Expand Down
2 changes: 2 additions & 0 deletions src/renderer/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ export interface ModelConfig {
model: string
description?: string
available: boolean
base_url?: string // Custom base URL for proxy/gateway support
custom?: boolean // Flag to identify custom models
}

// Subagent types (from deepagentsjs)
Expand Down