From a0e59f814bc6d71d9953104e9284a602681b7838 Mon Sep 17 00:00:00 2001 From: XiaoHuo888-hue Date: Sat, 22 Aug 2026 20:23:36 +0000 Subject: [PATCH] feat: add OrcaRouter as a named AI show notes provider Adds a named 'orcarouter' provider option to generate_ai_notes, mirroring the existing openai and anthropic providers. It uses the OpenAI SDK pointed at https://api.orcarouter.ai/v1 with the ORCAROUTER_API_KEY env var. - src/server/ai/orcarouter.ts: OpenAI-compatible client for OrcaRouter - generate_ai_notes provider union now includes 'orcarouter' - save_ai_notes_to_db stores the actual provider returned - document ORCAROUTER_API_KEY in .env.example and README Co-Authored-By: Claude Signed-off-by: XiaoHuo888-hue --- .env.example | 2 ++ README.md | 17 +++++++++-------- src/server/ai/db.ts | 2 +- src/server/ai/openai.ts | 16 +++++++++++++--- src/server/ai/orcarouter.test.ts | 12 ++++++++++++ src/server/ai/orcarouter.ts | 14 ++++++++++++++ 6 files changed, 51 insertions(+), 12 deletions(-) create mode 100644 src/server/ai/orcarouter.test.ts create mode 100644 src/server/ai/orcarouter.ts diff --git a/.env.example b/.env.example index 22244177d..2130460b7 100644 --- a/.env.example +++ b/.env.example @@ -10,6 +10,8 @@ PUBLIC_URL='localhost' SENTRY_AUTH_TOKEN='' # NEED_FOR_ROBOT_OVERLORD OPENAI_API_KEY='' +# OPTIONAL_ALTERNATIVE_AI_PROVIDER (OpenAI-compatible gateway; sk-orca-...) +ORCAROUTER_API_KEY='' # NEED_FOR_CACHING UPSPLASH_TOKEN='' UPSPLASH_URL='' diff --git a/README.md b/README.md index 6c2d4834e..78e8fba1c 100644 --- a/README.md +++ b/README.md @@ -104,14 +104,15 @@ These are the available media queries: ### Where to get your own Environment Variables -| Variable | Where to get it | Notes | -| ---------------------------- | ----------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | -| PUBLIC_GITHUB_ID, GH_SECRET | [Github Oauth Apps](https://github.com/settings/developers) | Create new OAuth App, set `http://localhost:5173/api/oauth/github/callback` as the redirect URL | -| DEEPGRAM_SECRET | [Deepgram](https://console.deepgram.com/) | | -| SENTRY_AUTH_TOKEN | [Sentry](https://docs.sentry.io/product/accounts/auth-tokens/) | | -| OPENAI_API_KEY | [Open AI](https://platform.openai.com/account/api-keys) | | -| UPSPLASH_TOKEN, UPSPLASH_URL | [https://upstash.com/](https://upstash.com/) | Create a redis DB after sign up in the console | -| YOUTUBE_API_KEY | [Google API Console](https://console.cloud.google.com/apis/credentials) | Create an API key, visit the library and enable "YouTube Data API v3" | +| Variable | Where to get it | Notes | +| ---------------------------- | ----------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | +| PUBLIC_GITHUB_ID, GH_SECRET | [Github Oauth Apps](https://github.com/settings/developers) | Create new OAuth App, set `http://localhost:5173/api/oauth/github/callback` as the redirect URL | +| DEEPGRAM_SECRET | [Deepgram](https://console.deepgram.com/) | | +| SENTRY_AUTH_TOKEN | [Sentry](https://docs.sentry.io/product/accounts/auth-tokens/) | | +| OPENAI_API_KEY | [Open AI](https://platform.openai.com/account/api-keys) | | +| ORCAROUTER_API_KEY | [OrcaRouter](https://www.orcarouter.ai) | OpenAI-compatible gateway (key starts with `sk-orca-`). AI show notes can be generated via OrcaRouter by passing `provider: 'orcarouter'`. | +| UPSPLASH_TOKEN, UPSPLASH_URL | [https://upstash.com/](https://upstash.com/) | Create a redis DB after sign up in the console | +| YOUTUBE_API_KEY | [Google API Console](https://console.cloud.google.com/apis/credentials) | Create an API key, visit the library and enable "YouTube Data API v3" | # Our Contributors diff --git a/src/server/ai/db.ts b/src/server/ai/db.ts index 90df82d61..1a5333234 100644 --- a/src/server/ai/db.ts +++ b/src/server/ai/db.ts @@ -30,7 +30,7 @@ export async function save_ai_notes_to_db(result: Result, show: Show) { timestamp: link.timestamp })) }, - provider: 'anthropic' + provider: result.provider } }); } diff --git a/src/server/ai/openai.ts b/src/server/ai/openai.ts index dd2d57034..ea91f93f2 100644 --- a/src/server/ai/openai.ts +++ b/src/server/ai/openai.ts @@ -4,6 +4,7 @@ import { encode } from 'gpt-3-encoder'; import { Configuration, OpenAIApi, type CreateChatCompletionRequest } from 'openai'; import wait from 'waait'; import { anthropic_completion, convert_openai_to_anthropic } from './anthropic'; +import { orca_completion } from './orcarouter'; import { create_condensed_prompt, summarize_prompt, summarize_prompt_2 } from './prompts'; import type { AIPodcastSummaryResponse, transcript_without_ai_notes_query } from './queries'; import type { SlimUtterance, TranscribedShow } from '../transcripts/types'; @@ -122,7 +123,7 @@ export async function condense( export async function generate_ai_notes( show: Prisma.ShowGetPayload, - provider: 'openai' | 'anthropic' = 'anthropic' + provider: 'openai' | 'anthropic' | 'orcarouter' = 'anthropic' ) { if (!show || !show.transcript?.utterances) { throw new Error(`No transcript found for show ${show.number}`); @@ -138,8 +139,8 @@ export async function generate_ai_notes( utterances: slim_utterance }, { - // anthropic doesnt need to condense - skip: provider === 'anthropic' + // anthropic and orcarouter dont need to condense + skip: provider === 'anthropic' || provider === 'orcarouter' } ); const condensed_transcript = formatAsTranscript(slim_utterances_with_condensed); @@ -177,6 +178,15 @@ export async function generate_ai_notes( const aparsed = JSON.parse(json_part) as AIPodcastSummaryResponse; return { ...aparsed, provider: 'anthropic' }; } + if (provider === 'orcarouter') { + console.log(`Using orcarouter for ${show.number}`); + const orca_result = await orca_completion(input); + console.log(orca_result.data); + const maybe_json = orca_result.data.choices.at(0)?.message?.content; + console.log(maybe_json); + const parsed = JSON.parse(maybe_json || '') as AIPodcastSummaryResponse; + return { ...parsed, provider: 'orcarouter' }; + } // OpenAI console.log(`Using openai for ${show.number}`); const completion = await openai.createChatCompletion(input).catch((err) => { diff --git a/src/server/ai/orcarouter.test.ts b/src/server/ai/orcarouter.test.ts new file mode 100644 index 000000000..0da97ebd0 --- /dev/null +++ b/src/server/ai/orcarouter.test.ts @@ -0,0 +1,12 @@ +import { describe, expect, it } from 'vitest'; +import { MODEL, ORCAROUTER_API_BASE } from './orcarouter'; + +describe('OrcaRouter provider', () => { + it('uses the OrcaRouter API base URL', () => { + expect(ORCAROUTER_API_BASE).toBe('https://api.orcarouter.ai/v1'); + }); + + it('uses the OrcaRouter auto model', () => { + expect(MODEL).toBe('orcarouter/auto'); + }); +}); diff --git a/src/server/ai/orcarouter.ts b/src/server/ai/orcarouter.ts new file mode 100644 index 000000000..5f5ecf710 --- /dev/null +++ b/src/server/ai/orcarouter.ts @@ -0,0 +1,14 @@ +import { Configuration, OpenAIApi, type CreateChatCompletionRequest } from 'openai'; + +export const ORCAROUTER_API_BASE = 'https://api.orcarouter.ai/v1'; +export const MODEL = 'orcarouter/auto'; + +const configuration = new Configuration({ + apiKey: process.env.ORCAROUTER_API_KEY, + basePath: ORCAROUTER_API_BASE +}); +export const orcarouter = new OpenAIApi(configuration); + +export async function orca_completion(input: CreateChatCompletionRequest) { + return orcarouter.createChatCompletion(input); +}