diff --git a/apps/x/apps/main/src/ipc.ts b/apps/x/apps/main/src/ipc.ts index b9064380..6823bd22 100644 --- a/apps/x/apps/main/src/ipc.ts +++ b/apps/x/apps/main/src/ipc.ts @@ -25,6 +25,8 @@ import { ServiceEvent } from '@x/shared/dist/service-events.js'; import container from '@x/core/dist/di/container.js'; import { listOnboardingModels } from '@x/core/dist/models/models-dev.js'; import { testModelConnection } from '@x/core/dist/models/models.js'; +import { isSignedIn } from '@x/core/dist/account/account.js'; +import { listGatewayModels } from '@x/core/dist/models/gateway.js'; import type { IModelConfigRepo } from '@x/core/dist/models/repo.js'; import type { IOAuthRepo } from '@x/core/dist/auth/repo.js'; import { IGranolaConfigRepo } from '@x/core/dist/knowledge/granola/repo.js'; @@ -375,6 +377,9 @@ export function setupIpcHandlers() { return { success: true }; }, 'models:list': async () => { + if (await isSignedIn()) { + return await listGatewayModels(); + } return await listOnboardingModels(); }, 'models:test': async (_event, args) => { diff --git a/apps/x/packages/core/src/account/account.ts b/apps/x/packages/core/src/account/account.ts new file mode 100644 index 00000000..4f33f611 --- /dev/null +++ b/apps/x/packages/core/src/account/account.ts @@ -0,0 +1,8 @@ +import container from '../di/container.js'; +import { IOAuthRepo } from '../auth/repo.js'; + +export async function isSignedIn(): Promise { + const oauthRepo = container.resolve('oauthRepo'); + const { tokens } = await oauthRepo.read('rowboat'); + return !!tokens; +} diff --git a/apps/x/packages/core/src/agents/runtime.ts b/apps/x/packages/core/src/agents/runtime.ts index 0264772d..cd85e9be 100644 --- a/apps/x/packages/core/src/agents/runtime.ts +++ b/apps/x/packages/core/src/agents/runtime.ts @@ -15,6 +15,8 @@ import { isBlocked, extractCommandNames } from "../application/lib/command-execu import container from "../di/container.js"; import { IModelConfigRepo } from "../models/repo.js"; import { createProvider } from "../models/models.js"; +import { isSignedIn } from "../account/account.js"; +import { getGatewayProvider } from "../models/gateway.js"; import { IAgentsRepo } from "./repo.js"; import { IMonotonicallyIncreasingIdGenerator } from "../application/lib/id-gen.js"; import { IBus } from "../application/lib/bus.js"; @@ -768,7 +770,9 @@ export async function* streamAgent({ const tools = await buildTools(agent); // set up provider + model - const provider = createProvider(modelConfig.provider); + const provider = await isSignedIn() + ? await getGatewayProvider() + : createProvider(modelConfig.provider); const knowledgeGraphAgents = ["note_creation", "email-draft", "meeting-prep", "labeling_agent", "note_tagging_agent"]; const modelId = (knowledgeGraphAgents.includes(state.agentName!) && modelConfig.knowledgeGraphModel) ? modelConfig.knowledgeGraphModel diff --git a/apps/x/packages/core/src/application/lib/builtin-tools.ts b/apps/x/packages/core/src/application/lib/builtin-tools.ts index 33e426c1..e50c4acd 100644 --- a/apps/x/packages/core/src/application/lib/builtin-tools.ts +++ b/apps/x/packages/core/src/application/lib/builtin-tools.ts @@ -16,6 +16,8 @@ import type { ToolContext } from "./exec-tool.js"; import { generateText } from "ai"; import { createProvider } from "../../models/models.js"; import { IModelConfigRepo } from "../../models/repo.js"; +import { isSignedIn } from "../../account/account.js"; +import { getGatewayProvider } from "../../models/gateway.js"; // Parser libraries are loaded dynamically inside parseFile.execute() // to avoid pulling pdfjs-dist's DOM polyfills into the main bundle. // Import paths are computed so esbuild cannot statically resolve them. @@ -632,7 +634,9 @@ export const BuiltinTools: z.infer = { // Resolve model config from DI container const modelConfigRepo = container.resolve('modelConfigRepo'); const modelConfig = await modelConfigRepo.getConfig(); - const provider = createProvider(modelConfig.provider); + const provider = await isSignedIn() + ? await getGatewayProvider() + : createProvider(modelConfig.provider); const model = provider.languageModel(modelConfig.model); const userPrompt = prompt || 'Convert this file to well-structured markdown.'; diff --git a/apps/x/packages/core/src/auth/providers.ts b/apps/x/packages/core/src/auth/providers.ts index fc3d302b..2fc644d9 100644 --- a/apps/x/packages/core/src/auth/providers.ts +++ b/apps/x/packages/core/src/auth/providers.ts @@ -1,6 +1,5 @@ import { z } from 'zod'; - -const SUPABASE_PROJECT_URL = 'http://127.0.0.1:54321'; +import { SUPABASE_PROJECT_URL } from '../config/env.js'; /** * Discovery configuration - how to get OAuth endpoints @@ -56,7 +55,7 @@ const providerConfigs: ProviderConfig = { rowboat: { discovery: { mode: 'issuer', - issuer: `${SUPABASE_PROJECT_URL}/.well-known/oauth-authorization-server`, + issuer: `${SUPABASE_PROJECT_URL}/auth/v1/.well-known/oauth-authorization-server`, }, client: { mode: 'dcr', diff --git a/apps/x/packages/core/src/config/env.ts b/apps/x/packages/core/src/config/env.ts new file mode 100644 index 00000000..ccb0d771 --- /dev/null +++ b/apps/x/packages/core/src/config/env.ts @@ -0,0 +1,5 @@ +export const ROWBOAT_AI_GATEWAY_BASE_URL = + process.env.ROWBOAT_AI_GATEWAY_BASE_URL || 'http://localhost:3002/v1'; + +export const SUPABASE_PROJECT_URL = + process.env.SUPABASE_PROJECT_URL || 'http://127.0.0.1:54321'; diff --git a/apps/x/packages/core/src/models/gateway.ts b/apps/x/packages/core/src/models/gateway.ts new file mode 100644 index 00000000..975f9129 --- /dev/null +++ b/apps/x/packages/core/src/models/gateway.ts @@ -0,0 +1,86 @@ +import { ProviderV2 } from '@ai-sdk/provider'; +import { createOpenRouter } from '@openrouter/ai-sdk-provider'; +import container from '../di/container.js'; +import { IOAuthRepo } from '../auth/repo.js'; +import { IClientRegistrationRepo } from '../auth/client-repo.js'; +import { getProviderConfig } from '../auth/providers.js'; +import * as oauthClient from '../auth/oauth-client.js'; +import { ROWBOAT_AI_GATEWAY_BASE_URL } from '../config/env.js'; + +async function getAccessToken(): Promise { + const oauthRepo = container.resolve('oauthRepo'); + const { tokens } = await oauthRepo.read('rowboat'); + if (!tokens) { + throw new Error('Not signed into Rowboat'); + } + + if (!oauthClient.isTokenExpired(tokens)) { + return tokens.access_token; + } + + if (!tokens.refresh_token) { + throw new Error('Rowboat token expired and no refresh token available. Please sign in again.'); + } + + const providerConfig = getProviderConfig('rowboat'); + if (providerConfig.discovery.mode !== 'issuer') { + throw new Error('Rowboat provider requires issuer discovery mode'); + } + + const clientRepo = container.resolve('clientRegistrationRepo'); + const registration = await clientRepo.getClientRegistration('rowboat'); + if (!registration) { + throw new Error('Rowboat client not registered. Please sign in again.'); + } + + const config = await oauthClient.discoverConfiguration( + providerConfig.discovery.issuer, + registration.client_id, + ); + + const refreshed = await oauthClient.refreshTokens( + config, + tokens.refresh_token, + tokens.scopes, + ); + await oauthRepo.upsert('rowboat', { tokens: refreshed }); + + return refreshed.access_token; +} + +export async function getGatewayProvider(): Promise { + const accessToken = await getAccessToken(); + return createOpenRouter({ + baseURL: ROWBOAT_AI_GATEWAY_BASE_URL, + apiKey: accessToken, + }); +} + +type ProviderSummary = { + id: string; + name: string; + models: Array<{ + id: string; + name?: string; + release_date?: string; + }>; +}; + +export async function listGatewayModels(): Promise<{ providers: ProviderSummary[] }> { + const accessToken = await getAccessToken(); + const response = await fetch(`${ROWBOAT_AI_GATEWAY_BASE_URL}/models`, { + headers: { Authorization: `Bearer ${accessToken}` }, + }); + if (!response.ok) { + throw new Error(`Gateway /v1/models failed: ${response.status}`); + } + const body = await response.json() as { data: Array<{ id: string }> }; + const models = body.data.map((m) => ({ id: m.id })); + return { + providers: [{ + id: 'rowboat', + name: 'Rowboat', + models, + }], + }; +} diff --git a/apps/x/packages/core/src/models/models.ts b/apps/x/packages/core/src/models/models.ts index ab332fb6..7754bd8e 100644 --- a/apps/x/packages/core/src/models/models.ts +++ b/apps/x/packages/core/src/models/models.ts @@ -8,6 +8,8 @@ import { createOpenRouter } from '@openrouter/ai-sdk-provider'; import { createOpenAICompatible } from '@ai-sdk/openai-compatible'; import { LlmModelConfig, LlmProvider } from "@x/shared/dist/models.js"; import z from "zod"; +import { isSignedIn } from "../account/account.js"; +import { getGatewayProvider } from "./gateway.js"; export const Provider = LlmProvider; export const ModelConfig = LlmModelConfig; @@ -78,7 +80,9 @@ export async function testModelConnection( const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), effectiveTimeout); try { - const provider = createProvider(providerConfig); + const provider = await isSignedIn() + ? await getGatewayProvider() + : createProvider(providerConfig); const languageModel = provider.languageModel(model); await generateText({ model: languageModel,