diff --git a/common/src/constants/requesty.ts b/common/src/constants/requesty.ts new file mode 100644 index 0000000000..a8421599e4 --- /dev/null +++ b/common/src/constants/requesty.ts @@ -0,0 +1,16 @@ +/** + * Constants for the optional direct Requesty provider route. + * + * Requesty (https://requesty.ai) exposes an OpenAI-compatible router at + * https://router.requesty.ai/v1. When `REQUESTY_API_KEY` is set, the SDK can + * route chat completions directly to Requesty using the same OpenAI-compatible + * language model shim used elsewhere, instead of going through the Codebuff + * backend. Model ids use the same `provider/model` form as OpenRouter + * (e.g. `openai/gpt-4o-mini`, `anthropic/claude-sonnet-4.5`). + */ + +/** Base URL for the Requesty OpenAI-compatible router. */ +export const REQUESTY_BASE_URL = 'https://router.requesty.ai/v1' + +/** Environment variable holding the Requesty API key. */ +export const REQUESTY_ENV_VAR = 'REQUESTY_API_KEY' diff --git a/docs/agents-and-tools.md b/docs/agents-and-tools.md index 4ea7475896..f95147def3 100644 --- a/docs/agents-and-tools.md +++ b/docs/agents-and-tools.md @@ -19,3 +19,25 @@ base-lite "fix this bug" - Tool definitions live in `common/src/tools` and are executed via the SDK helpers + agent-runtime. +## Model providers + +Model ids use the `provider/model` form (e.g. `openai/gpt-4o-mini`, +`anthropic/claude-sonnet-4.5`). By default, requests are sent through the +Codebuff backend, which routes to the upstream provider (OpenRouter). + +### Requesty (direct route) + +[Requesty](https://requesty.ai) is an OpenAI-compatible router. Setting the +`REQUESTY_API_KEY` environment variable routes chat completions directly to the +Requesty router (`https://router.requesty.ai/v1`) instead of the Codebuff +backend, using the same `provider/model` ids. This mirrors the existing direct +ChatGPT OAuth route in `sdk/src/impl/model-provider.ts`. + +- Router base URL: `https://router.requesty.ai/v1` +- API keys: https://app.requesty.ai/api-keys +- Model list: https://app.requesty.ai/router/list +- Docs: https://docs.requesty.ai + +When `REQUESTY_API_KEY` is unset, behavior is unchanged. + + diff --git a/sdk/README.md b/sdk/README.md index ff7d0ba960..66d99a7ae0 100644 --- a/sdk/README.md +++ b/sdk/README.md @@ -262,6 +262,31 @@ The `RunState` object contains: - `sessionState`: Internal state to be passed to the next run - `output`: The agent's output (text, error, or other types) +## Model providers + +By default, the SDK sends requests through the Codebuff backend, which routes to +the upstream model provider (OpenRouter). Model ids use the `provider/model` +form, e.g. `openai/gpt-4o-mini` or `anthropic/claude-sonnet-4.5`. + +### Requesty (direct, OpenAI-compatible) + +[Requesty](https://requesty.ai) exposes an OpenAI-compatible router. When the +`REQUESTY_API_KEY` environment variable is set, the SDK routes chat completions +directly to the Requesty router (`https://router.requesty.ai/v1`) instead of the +Codebuff backend, using the same `provider/model` ids: + +```bash +export REQUESTY_API_KEY="sk-..." # from https://app.requesty.ai/api-keys +``` + +- Router base URL: `https://router.requesty.ai/v1` +- API keys: https://app.requesty.ai/api-keys +- Available models: https://app.requesty.ai/router/list +- Docs: https://docs.requesty.ai + +When `REQUESTY_API_KEY` is unset, behavior is unchanged (requests go through the +Codebuff backend). + ## License MIT diff --git a/sdk/src/env.ts b/sdk/src/env.ts index 033e3f245d..29a81ccd61 100644 --- a/sdk/src/env.ts +++ b/sdk/src/env.ts @@ -7,6 +7,7 @@ import { BYOK_OPENROUTER_ENV_VAR } from '@codebuff/common/constants/byok' import { CHATGPT_OAUTH_TOKEN_ENV_VAR } from '@codebuff/common/constants/chatgpt-oauth' +import { REQUESTY_ENV_VAR } from '@codebuff/common/constants/requesty' import { API_KEY_ENV_VAR } from '@codebuff/common/constants/paths' import { getBaseEnv } from '@codebuff/common/env-process' @@ -42,6 +43,16 @@ export const getByokOpenrouterApiKeyFromEnv = (): string | undefined => { return process.env[BYOK_OPENROUTER_ENV_VAR] } +/** + * Get the Requesty API key from the environment. + * + * When set, the SDK routes chat completions directly to the Requesty + * OpenAI-compatible router instead of going through the Codebuff backend. + */ +export const getRequestyApiKeyFromEnv = (): string | undefined => { + return process.env[REQUESTY_ENV_VAR] +} + /** * Get ChatGPT OAuth token from environment variable. */ diff --git a/sdk/src/impl/__tests__/model-provider-requesty.test.ts b/sdk/src/impl/__tests__/model-provider-requesty.test.ts new file mode 100644 index 0000000000..07e379df84 --- /dev/null +++ b/sdk/src/impl/__tests__/model-provider-requesty.test.ts @@ -0,0 +1,56 @@ +import { describe, expect, test, beforeEach, afterEach } from 'bun:test' + +import { REQUESTY_ENV_VAR } from '@codebuff/common/constants/requesty' + +describe('getModelForRequest Requesty direct route', () => { + const previousRequestyKey = process.env[REQUESTY_ENV_VAR] + + beforeEach(() => { + delete process.env[REQUESTY_ENV_VAR] + }) + + afterEach(() => { + if (previousRequestyKey === undefined) { + delete process.env[REQUESTY_ENV_VAR] + } else { + process.env[REQUESTY_ENV_VAR] = previousRequestyKey + } + }) + + async function importFresh() { + const mod = await import('../model-provider') + mod.resetChatGptOAuthRateLimit() + return mod + } + + test('routes to Requesty when REQUESTY_API_KEY is set', async () => { + process.env[REQUESTY_ENV_VAR] = 'test-requesty-key' + + const { getModelForRequest } = await importFresh() + + const result = await getModelForRequest({ + apiKey: 'test-codebuff-key', + model: 'openai/gpt-4o-mini', + }) + + expect(result.isChatGptOAuth).toBe(false) + expect(typeof result.model).not.toBe('string') + if (typeof result.model !== 'string') { + expect(result.model.provider).toBe('requesty') + } + }) + + test('uses Codebuff backend when REQUESTY_API_KEY is not set', async () => { + const { getModelForRequest } = await importFresh() + + const result = await getModelForRequest({ + apiKey: 'test-codebuff-key', + model: 'openai/gpt-4o-mini', + }) + + expect(result.isChatGptOAuth).toBe(false) + if (typeof result.model !== 'string') { + expect(result.model.provider).toBe('codebuff') + } + }) +}) diff --git a/sdk/src/impl/model-provider.ts b/sdk/src/impl/model-provider.ts index 268c7394d0..187ff48273 100644 --- a/sdk/src/impl/model-provider.ts +++ b/sdk/src/impl/model-provider.ts @@ -10,6 +10,7 @@ import path from 'path' import { BYOK_OPENROUTER_HEADER } from '@codebuff/common/constants/byok' import { isFreeMode } from '@codebuff/common/constants/free-agents' +import { REQUESTY_BASE_URL } from '@codebuff/common/constants/requesty' import { CHATGPT_BACKEND_BASE_URL, CHATGPT_OAUTH_ENABLED, @@ -24,7 +25,10 @@ import { import { WEBSITE_URL } from '../constants' import { getValidChatGptOAuthCredentials } from '../credentials' -import { getByokOpenrouterApiKeyFromEnv } from '../env' +import { + getByokOpenrouterApiKeyFromEnv, + getRequestyApiKeyFromEnv, +} from '../env' import { createChatGptBackendFetch, extractChatGptAccountId, @@ -117,6 +121,18 @@ export async function getModelForRequest( ): Promise { const { apiKey, model, skipChatGptOAuth, costMode } = params + // Direct Requesty route: when REQUESTY_API_KEY is set, send chat completions + // straight to the Requesty OpenAI-compatible router instead of the Codebuff + // backend. Model ids use the same `provider/model` form (e.g. + // "openai/gpt-4o-mini"), so no remapping is required. + const requestyApiKey = getRequestyApiKeyFromEnv() + if (requestyApiKey) { + return { + model: createRequestyModel(model, requestyApiKey), + isChatGptOAuth: false, + } + } + // Check if we should use ChatGPT OAuth direct // Only attempt for allowlisted models; non-allowlisted models silently fall through to backend. if ( @@ -191,6 +207,30 @@ function createOpenAIOAuthModel( }) } +/** + * Create a model that routes directly to the Requesty OpenAI-compatible router + * (https://router.requesty.ai/v1). Used when REQUESTY_API_KEY is set. + * + * This mirrors createOpenAIOAuthModel: it builds an OpenAICompatibleChatLanguageModel + * pointed at a fixed upstream base URL with a Bearer API key, rather than going + * through the Codebuff backend. Model ids use the same `provider/model` form as + * OpenRouter (e.g. "openai/gpt-4o-mini"). + */ +function createRequestyModel(model: string, apiKey: string): LanguageModel { + return new OpenAICompatibleChatLanguageModel(model, { + provider: 'requesty', + url: ({ path }) => `${REQUESTY_BASE_URL}${path}`, + headers: () => ({ + Authorization: `Bearer ${apiKey}`, + 'Content-Type': 'application/json', + 'user-agent': `ai-sdk/openai-compatible/${VERSION}/codebuff-requesty`, + }), + fetch: undefined, + includeUsage: undefined, + supportsStructuredOutputs: true, + }) +} + /** * Create a model that routes through the Codebuff backend. * This is the existing behavior - requests go to Codebuff backend which forwards to OpenRouter.