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
6 changes: 6 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ const TOML_CONFIG_PATH = AUTOHAND_FILES.configToml;
const YAML_CONFIG_PATH = AUTOHAND_FILES.configYaml;
const YML_CONFIG_PATH = AUTOHAND_FILES.configYml;
const DEFAULT_BASE_URL = "https://openrouter.ai/api/v1";
const DEFAULT_OPENPATHS_URL = "https://openpaths.io/v1";
const DEFAULT_OLLAMA_URL = "http://localhost:11434";
const DEFAULT_LLAMACPP_URL = "http://localhost:8080";
const DEFAULT_OPENAI_URL = "https://api.openai.com/v1";
Expand Down Expand Up @@ -62,6 +63,7 @@ function normalizeProviderName(provider: unknown): ProviderName | undefined {

const validProviders: readonly ProviderName[] = [
"openrouter",
"openpaths",
"ollama",
"llamacpp",
"openai",
Expand Down Expand Up @@ -646,6 +648,7 @@ function isModernConfig(
): config is AutohandConfig {
return (
typeof (config as AutohandConfig).openrouter === "object" ||
typeof (config as AutohandConfig).openpaths === "object" ||
typeof (config as AutohandConfig).ollama === "object" ||
typeof (config as AutohandConfig).llamacpp === "object" ||
typeof (config as AutohandConfig).openai === "object" ||
Expand Down Expand Up @@ -829,6 +832,7 @@ export function getProviderConfig(

const configByProvider: Record<ProviderName, ProviderSettings | undefined> = {
openrouter: config.openrouter,
openpaths: config.openpaths,
ollama: config.ollama,
llamacpp: config.llamacpp,
openai: config.openai,
Expand Down Expand Up @@ -870,6 +874,7 @@ export function getProviderConfig(
}
} else if (
chosen === "openrouter" ||
chosen === "openpaths" ||
chosen === "llmgateway" ||
chosen === "zai" ||
chosen === "nvidia" ||
Expand Down Expand Up @@ -912,6 +917,7 @@ function defaultBaseUrlFor(
port?: number,
): string | undefined {
if (provider === "openrouter") return DEFAULT_BASE_URL;
if (provider === "openpaths") return DEFAULT_OPENPATHS_URL;
if (provider === "llmgateway") return DEFAULT_LLMGATEWAY_URL;
if (provider === "zai") return DEFAULT_ZAI_URL;
if (provider === "deepseek") return DEFAULT_DEEPSEEK_URL;
Expand Down
3 changes: 3 additions & 0 deletions src/core/agent/ProviderConfigManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2810,6 +2810,9 @@ export class ProviderConfigManager {
openrouter:
this.runtime.config.openrouter ??
(this.runtime.config.openrouter = { apiKey: "", model }),
openpaths:
this.runtime.config.openpaths ??
(this.runtime.config.openpaths = { apiKey: "", model }),
ollama:
this.runtime.config.ollama ?? (this.runtime.config.ollama = { model }),
llamacpp:
Expand Down
2 changes: 2 additions & 0 deletions src/onboarding/setupWizard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2051,6 +2051,7 @@ export class SetupWizard {
private getDefaultModel(provider: ProviderName): string {
const defaults: Record<ProviderName, string> = {
openrouter: 'nvidia/nemotron-3-super-120b-a12b:free',
openpaths: 'openpaths/auto',

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Treat OpenPaths as an API-key provider during onboarding

When a new user selects OpenPaths in the setup wizard, requiresApiKey() still excludes openpaths, so onboarding skips promptApiKey()/validation and complete() writes only a model/baseUrl config. getProviderConfig() now requires an API key for openpaths, so the generated config is incomplete and OpenPaths requests run without authorization until the user hand-edits the config.

Useful? React with 👍 / 👎.

openai: 'gpt-5.4',
ollama: 'llama3.2:latest',
llamacpp: 'local',
Expand All @@ -2071,6 +2072,7 @@ export class SetupWizard {
private getDefaultBaseUrl(provider: ProviderName): string {
const urls: Record<ProviderName, string> = {
openrouter: 'https://openrouter.ai/api/v1',
openpaths: 'https://openpaths.io/v1',
openai: 'https://api.openai.com/v1',
ollama: 'http://localhost:11434',
llamacpp: 'http://localhost:8080',
Expand Down
85 changes: 85 additions & 0 deletions src/providers/OpenPathsProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/**
* @license
* Copyright 2025 Autohand AI LLC
* SPDX-License-Identifier: Apache-2.0
*/

import { OpenRouterClient } from "./OpenRouterClient.js";
import type { LLMProvider } from "./LLMProvider.js";
import type {
LLMRequest,
LLMResponse,
OpenPathsSettings,
NetworkSettings,
} from "../types.js";

const OPENPATHS_BASE_URL = "https://openpaths.io/v1";
const OPENPATHS_MODELS_URL = "https://openpaths.io/v1/models";

/**
* OpenPaths is an OpenAI-compatible model gateway (https://openpaths.io).
* It reuses the generic OpenAI-compatible client used by OpenRouter, pointed
* at the OpenPaths base URL.
*/
export class OpenPathsProvider implements LLMProvider {
private client: OpenRouterClient;
private model: string;

constructor(config: OpenPathsSettings, networkSettings?: NetworkSettings) {
this.client = new OpenRouterClient(
{ ...config, baseUrl: config.baseUrl ?? OPENPATHS_BASE_URL },
networkSettings,
);
this.model = config.model;
}

getName(): string {
return "openpaths";
}

setModel(model: string): void {
this.model = model;
this.client.setDefaultModel(model);
}

async listModels(): Promise<string[]> {
try {
const response = await fetch(OPENPATHS_MODELS_URL, {
headers: {
"Content-Type": "application/json",
},
signal: AbortSignal.timeout(10000),
});

if (response.ok) {
const data = (await response.json()) as { data?: Array<{ id?: string }> };
const ids = (Array.isArray(data?.data) ? data.data : [])
.map((model) => model.id)
.filter((id): id is string => Boolean(id));

if (ids.length > 0) {
return ids;
}
}
} catch {
// Fall through to the static fallback list below.
}

return [
"openpaths/auto",
"openpaths/auto-code",
"openpaths/auto-fast",
"openpaths/auto-reasoning",
];
}

async isAvailable(): Promise<boolean> {
// For OpenPaths, we can't easily check without making a request
// Return true if we have an API key
return true;
}

async complete(request: LLMRequest): Promise<LLMResponse> {
return this.client.complete(request);
}
}
11 changes: 9 additions & 2 deletions src/providers/ProviderFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { OllamaProvider } from './OllamaProvider.js';
import { OpenAIProvider } from './OpenAIProvider.js';
import { LlamaCppProvider } from './LlamaCppProvider.js';
import { OpenRouterProvider } from './OpenRouterProvider.js';
import { OpenPathsProvider } from './OpenPathsProvider.js';
import { MLXProvider } from './MLXProvider.js';
import { LLMGatewayProvider } from './LLMGatewayProvider.js';
import { AzureProvider } from './AzureProvider.js';
Expand Down Expand Up @@ -154,6 +155,12 @@ export class ProviderFactory {
}
return new BedrockProvider(config.bedrock);

case 'openpaths':
if (!config.openpaths) {
return new UnconfiguredProvider('openpaths');
}
return new OpenPathsProvider(config.openpaths, config.network);

case 'openrouter':
default:
if (!config.openrouter) {
Expand All @@ -169,7 +176,7 @@ export class ProviderFactory {
*/
static getProviderNames(config?: Pick<AutohandConfig, 'features'> | null): ProviderName[] {
// Sorted DESC by display name: Z.ai, xAI, Vertex AI, NVIDIA, OpenRouter, OpenAI, Ollama, MLX, LLM Gateway, llama.cpp, DeepSeek, Cerebras, Bedrock, Azure
const providers: ProviderName[] = ['zai', 'xai', 'vertexai', 'nvidia', 'openrouter', 'openai', 'ollama', 'llmgateway', 'llamacpp', 'deepseek', 'cerebras', 'azure'];
const providers: ProviderName[] = ['zai', 'xai', 'vertexai', 'nvidia', 'openrouter', 'openpaths', 'openai', 'ollama', 'llmgateway', 'llamacpp', 'deepseek', 'cerebras', 'azure'];

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Wire OpenPaths into the interactive provider configurator

Adding openpaths to getProviderNames() exposes it in the interactive provider picker, but checked ProviderConfigManager.configureProvider() and there is no openpaths case; selecting it when unconfigured prints the setup message and then returns without prompting for a key/model. The same manager also omits it from the cloud/API-key provider checks, so a partial model-only config is treated as configured and never offers an API-key action.

Useful? React with 👍 / 👎.

if (isAwsBedrockProviderEnabled(config)) {
providers.splice(providers.indexOf('azure'), 0, 'bedrock');
}
Expand All @@ -189,7 +196,7 @@ export class ProviderFactory {
return false;
}

const allProviders: ProviderName[] = ['openrouter', 'ollama', 'openai', 'llamacpp', 'mlx', 'llmgateway', 'azure', 'zai', 'vertexai', 'xai', 'cerebras', 'nvidia', 'deepseek', 'bedrock'];
const allProviders: ProviderName[] = ['openrouter', 'openpaths', 'ollama', 'openai', 'llamacpp', 'mlx', 'llmgateway', 'azure', 'zai', 'vertexai', 'xai', 'cerebras', 'nvidia', 'deepseek', 'bedrock'];
return allProviders.includes(name as ProviderName);
}
}
7 changes: 6 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ type Primitive = string | number | boolean | null;

export type MessageRole = 'system' | 'user' | 'assistant' | 'tool';

export type ProviderName = 'openrouter' | 'ollama' | 'llamacpp' | 'openai' | 'mlx' | 'llmgateway' | 'azure' | 'zai' | 'vertexai' | 'xai' | 'cerebras' | 'nvidia' | 'deepseek' | 'bedrock';
export type ProviderName = 'openrouter' | 'openpaths' | 'ollama' | 'llamacpp' | 'openai' | 'mlx' | 'llmgateway' | 'azure' | 'zai' | 'vertexai' | 'xai' | 'cerebras' | 'nvidia' | 'deepseek' | 'bedrock';

export type AzureAuthMethod = 'api-key' | 'entra-id' | 'managed-identity';
export type OpenAIAuthMode = 'api-key' | 'chatgpt';
Expand All @@ -56,6 +56,10 @@ export interface OpenRouterSettings extends ProviderSettings {
apiKey: string;
}

export interface OpenPathsSettings extends ProviderSettings {
apiKey: string;
}

export interface LLMGatewaySettings extends ProviderSettings {
apiKey: string;
}
Expand Down Expand Up @@ -650,6 +654,7 @@ export interface ChromeConfigSettings {
export interface AutohandConfig {
provider?: ProviderName;
openrouter?: OpenRouterSettings;
openpaths?: OpenPathsSettings;
ollama?: ProviderSettings;
llamacpp?: ProviderSettings;
openai?: OpenAISettings;
Expand Down
29 changes: 29 additions & 0 deletions tests/providers/ProviderFactory.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ describe("ProviderFactory", () => {
"vertexai",
"nvidia",
"openrouter",
"openpaths",
"openai",
"ollama",
"llmgateway",
Expand Down Expand Up @@ -194,13 +195,41 @@ describe("ProviderFactory", () => {

expect(provider.getName()).toBe("openrouter");
});

it("should create OpenPathsProvider when openpaths is configured", () => {
const config: AutohandConfig = {
provider: "openpaths",
openpaths: {
apiKey: "test-key",
model: "openpaths/auto",
},
};

const provider = ProviderFactory.create(config);

expect(provider.getName()).toBe("openpaths");
});

it("should return UnconfiguredProvider when openpaths config is missing", () => {
const config: AutohandConfig = {
provider: "openpaths",
};

const provider = ProviderFactory.create(config);

expect(provider.getName()).toBe("unconfigured");
});
});

describe("isValidProvider()", () => {
it("should return true for openrouter", () => {
expect(ProviderFactory.isValidProvider("openrouter")).toBe(true);
});

it("should return true for openpaths", () => {
expect(ProviderFactory.isValidProvider("openpaths")).toBe(true);
});

it("should return true for ollama", () => {
expect(ProviderFactory.isValidProvider("ollama")).toBe(true);
});
Expand Down