diff --git a/packages/backend/convex/aiProviderProfiles.ts b/packages/backend/convex/aiProviderProfiles.ts index 7686b62..0e2b6d7 100644 --- a/packages/backend/convex/aiProviderProfiles.ts +++ b/packages/backend/convex/aiProviderProfiles.ts @@ -24,6 +24,7 @@ const provider = v.union( const authType = v.union( v.literal('api_key'), v.literal('opencode_auth_json'), + v.literal('anthropic_oauth_json'), v.literal('none'), ); diff --git a/packages/backend/convex/aiProviderProfilesNode.ts b/packages/backend/convex/aiProviderProfilesNode.ts index a91cc4d..3bae8d6 100644 --- a/packages/backend/convex/aiProviderProfilesNode.ts +++ b/packages/backend/convex/aiProviderProfilesNode.ts @@ -24,6 +24,7 @@ const provider = v.union( const authType = v.union( v.literal('api_key'), v.literal('opencode_auth_json'), + v.literal('anthropic_oauth_json'), v.literal('none'), ); diff --git a/packages/backend/convex/runtimeSupport.ts b/packages/backend/convex/runtimeSupport.ts new file mode 100644 index 0000000..f30e862 --- /dev/null +++ b/packages/backend/convex/runtimeSupport.ts @@ -0,0 +1,23 @@ +import type { Doc } from './_generated/dataModel'; + +export type AgentRuntimeName = 'codex' | 'opencode' | 'claude'; + +// Which runtimes a provider profile can drive. +export const runtimesForProfile = ( + profile: Pick, 'provider' | 'authType'>, +): AgentRuntimeName[] => { + if ( + profile.provider === 'opencode_openai_login' || + profile.authType === 'opencode_auth_json' + ) { + return ['codex']; // ChatGPT-login snapshot → Codex CLI only + } + if (profile.provider === 'anthropic') { + return profile.authType === 'anthropic_oauth_json' + ? ['claude'] + : ['claude', 'opencode']; + } + if (profile.provider === 'openai') return ['opencode', 'codex']; + // Every remaining API-key provider is OpenAI-compatible → OpenCode. + return ['opencode']; +}; diff --git a/packages/backend/convex/schema.ts b/packages/backend/convex/schema.ts index 8d4cabb..824c8c0 100644 --- a/packages/backend/convex/schema.ts +++ b/packages/backend/convex/schema.ts @@ -389,6 +389,7 @@ const applicationTables = { authType: v.union( v.literal('api_key'), v.literal('opencode_auth_json'), + v.literal('anthropic_oauth_json'), v.literal('none'), ), encryptedSecret: v.optional(v.string()), @@ -468,9 +469,14 @@ const applicationTables = { spoonId: v.id('spoons'), ownerId: v.id('users'), enabled: v.boolean(), - // Legacy records may contain openai_direct. New writes use opencode only. + // Legacy records may contain openai_direct (deprecated, never written). runtime: v.optional( - v.union(v.literal('opencode'), v.literal('openai_direct')), + v.union( + v.literal('openai_direct'), + v.literal('opencode'), + v.literal('codex'), + v.literal('claude'), + ), ), defaultBaseBranch: v.optional(v.string()), branchPrefix: v.string(), @@ -532,9 +538,14 @@ const applicationTables = { v.literal('timed_out'), ), prompt: v.string(), - // Legacy jobs may contain openai_direct. New jobs use opencode only. + // Legacy jobs may contain openai_direct (deprecated, never written). runtime: v.optional( - v.union(v.literal('openai_direct'), v.literal('opencode')), + v.union( + v.literal('openai_direct'), + v.literal('opencode'), + v.literal('codex'), + v.literal('claude'), + ), ), workspaceStatus: v.optional( v.union( diff --git a/packages/backend/tests/unit/runtime-support.test.ts b/packages/backend/tests/unit/runtime-support.test.ts new file mode 100644 index 0000000..faf8b44 --- /dev/null +++ b/packages/backend/tests/unit/runtime-support.test.ts @@ -0,0 +1,41 @@ +import { describe, expect, test } from 'vitest'; + +import { runtimesForProfile } from '../../convex/runtimeSupport'; + +describe('runtimesForProfile', () => { + test('anthropic + api_key drives claude and opencode', () => { + expect( + runtimesForProfile({ provider: 'anthropic', authType: 'api_key' }), + ).toEqual(['claude', 'opencode']); + }); + + test('opencode_openai_login snapshot drives codex only', () => { + expect( + runtimesForProfile({ + provider: 'opencode_openai_login', + authType: 'opencode_auth_json', + }), + ).toEqual(['codex']); + }); + + test('anthropic + anthropic_oauth_json drives claude only', () => { + expect( + runtimesForProfile({ + provider: 'anthropic', + authType: 'anthropic_oauth_json', + }), + ).toEqual(['claude']); + }); + + test('openrouter + api_key drives opencode only', () => { + expect( + runtimesForProfile({ provider: 'openrouter', authType: 'api_key' }), + ).toEqual(['opencode']); + }); + + test('openai + api_key drives opencode and codex', () => { + expect( + runtimesForProfile({ provider: 'openai', authType: 'api_key' }), + ).toEqual(['opencode', 'codex']); + }); +});