|
6 | 6 | FREEBUFF_GLM_MODEL_ID, |
7 | 7 | isFreebuffDeploymentHours, |
8 | 8 | } from '@codebuff/common/constants/freebuff-models' |
| 9 | +import { env } from '@codebuff/internal/env' |
9 | 10 | import { formatQuotaResetCountdown, postChatCompletions } from '../_post' |
10 | 11 | import { |
11 | 12 | checkFreeModeRateLimit, |
@@ -1075,6 +1076,116 @@ describe('/api/v1/chat/completions POST endpoint', () => { |
1075 | 1076 | }) |
1076 | 1077 |
|
1077 | 1078 | describe('Successful responses', () => { |
| 1079 | + const withCanopyWaveApiKey = async (testFn: () => Promise<void>) => { |
| 1080 | + const previousCanopyWaveApiKey = env.CANOPYWAVE_API_KEY |
| 1081 | + env.CANOPYWAVE_API_KEY = 'test' |
| 1082 | + try { |
| 1083 | + await testFn() |
| 1084 | + } finally { |
| 1085 | + env.CANOPYWAVE_API_KEY = previousCanopyWaveApiKey |
| 1086 | + } |
| 1087 | + } |
| 1088 | + |
| 1089 | + const createCanopyWaveFallbackRequest = (stream: boolean) => |
| 1090 | + new NextRequest('http://localhost:3000/api/v1/chat/completions', { |
| 1091 | + method: 'POST', |
| 1092 | + headers: { Authorization: 'Bearer test-api-key-123' }, |
| 1093 | + body: JSON.stringify({ |
| 1094 | + model: 'minimax/minimax-m2.5', |
| 1095 | + stream, |
| 1096 | + codebuff_metadata: { |
| 1097 | + run_id: 'run-123', |
| 1098 | + client_id: 'test-client-id-123', |
| 1099 | + client_request_id: 'test-client-session-id-123', |
| 1100 | + }, |
| 1101 | + }), |
| 1102 | + }) |
| 1103 | + |
| 1104 | + const createCanopyWaveNoWorkersThenFireworksFetch = (stream: boolean) => { |
| 1105 | + const fetchedBodies: Record<string, unknown>[] = [] |
| 1106 | + const fetch = mock( |
| 1107 | + async (_url: string | URL | Request, init?: RequestInit) => { |
| 1108 | + fetchedBodies.push(JSON.parse(init?.body as string)) |
| 1109 | + |
| 1110 | + if (fetchedBodies.length === 1) { |
| 1111 | + return Response.json( |
| 1112 | + { |
| 1113 | + error: { |
| 1114 | + message: 'No available workers', |
| 1115 | + code: 'no_available_workers', |
| 1116 | + }, |
| 1117 | + }, |
| 1118 | + { status: 503 }, |
| 1119 | + ) |
| 1120 | + } |
| 1121 | + |
| 1122 | + if (!stream) { |
| 1123 | + return Response.json({ |
| 1124 | + id: 'test-id', |
| 1125 | + model: 'accounts/fireworks/models/minimax-m2p5', |
| 1126 | + choices: [{ message: { content: 'fireworks response' } }], |
| 1127 | + usage: { |
| 1128 | + prompt_tokens: 10, |
| 1129 | + completion_tokens: 20, |
| 1130 | + total_tokens: 30, |
| 1131 | + }, |
| 1132 | + }) |
| 1133 | + } |
| 1134 | + |
| 1135 | + const encoder = new TextEncoder() |
| 1136 | + const fireworksStream = new ReadableStream({ |
| 1137 | + start(controller) { |
| 1138 | + controller.enqueue( |
| 1139 | + encoder.encode( |
| 1140 | + 'data: {"id":"test-id","model":"accounts/fireworks/models/minimax-m2p5","choices":[{"delta":{"content":"test"}}]}\n\n', |
| 1141 | + ), |
| 1142 | + ) |
| 1143 | + controller.enqueue(encoder.encode('data: [DONE]\n\n')) |
| 1144 | + controller.close() |
| 1145 | + }, |
| 1146 | + }) |
| 1147 | + |
| 1148 | + return new Response(fireworksStream, { |
| 1149 | + status: 200, |
| 1150 | + headers: { 'Content-Type': 'text/event-stream' }, |
| 1151 | + }) |
| 1152 | + }, |
| 1153 | + ) as unknown as typeof globalThis.fetch |
| 1154 | + |
| 1155 | + return { fetch, fetchedBodies } |
| 1156 | + } |
| 1157 | + |
| 1158 | + const postCanopyWaveFallbackRequest = async ({ |
| 1159 | + fetch, |
| 1160 | + stream, |
| 1161 | + }: { |
| 1162 | + fetch: typeof globalThis.fetch |
| 1163 | + stream: boolean |
| 1164 | + }) => |
| 1165 | + postChatCompletions({ |
| 1166 | + req: createCanopyWaveFallbackRequest(stream), |
| 1167 | + getUserInfoFromApiKey: mockGetUserInfoFromApiKey, |
| 1168 | + logger: mockLogger, |
| 1169 | + trackEvent: mockTrackEvent, |
| 1170 | + getUserUsageData: mockGetUserUsageData, |
| 1171 | + getAgentRunFromId: mockGetAgentRunFromId, |
| 1172 | + fetch, |
| 1173 | + insertMessageBigquery: mockInsertMessageBigquery, |
| 1174 | + loggerWithContext: mockLoggerWithContext, |
| 1175 | + checkSessionAdmissible: mockCheckSessionAdmissibleAllow, |
| 1176 | + }) |
| 1177 | + |
| 1178 | + const expectCanopyWaveThenFireworks = ( |
| 1179 | + fetchedBodies: Record<string, unknown>[], |
| 1180 | + ) => { |
| 1181 | + expect(fetchedBodies).toHaveLength(2) |
| 1182 | + expect(fetchedBodies[0].model).toBe('minimax/minimax-m2.5') |
| 1183 | + expect(fetchedBodies[1].model).toBe( |
| 1184 | + 'accounts/fireworks/models/minimax-m2p5', |
| 1185 | + ) |
| 1186 | + expect(mockLogger.warn).toHaveBeenCalled() |
| 1187 | + } |
| 1188 | + |
1078 | 1189 | it('returns stream with correct headers', async () => { |
1079 | 1190 | const req = new NextRequest( |
1080 | 1191 | 'http://localhost:3000/api/v1/chat/completions', |
@@ -1158,6 +1269,48 @@ describe('/api/v1/chat/completions POST endpoint', () => { |
1158 | 1269 | }, |
1159 | 1270 | FETCH_PATH_TEST_TIMEOUT_MS, |
1160 | 1271 | ) |
| 1272 | + |
| 1273 | + it( |
| 1274 | + 'falls back to Fireworks when CanopyWave has no available workers for non-streaming requests', |
| 1275 | + async () => { |
| 1276 | + await withCanopyWaveApiKey(async () => { |
| 1277 | + const { fetch, fetchedBodies } = |
| 1278 | + createCanopyWaveNoWorkersThenFireworksFetch(false) |
| 1279 | + const response = await postCanopyWaveFallbackRequest({ |
| 1280 | + fetch, |
| 1281 | + stream: false, |
| 1282 | + }) |
| 1283 | + |
| 1284 | + expect(response.status).toBe(200) |
| 1285 | + expectCanopyWaveThenFireworks(fetchedBodies) |
| 1286 | + |
| 1287 | + const body = await response.json() |
| 1288 | + expect(body.model).toBe('minimax/minimax-m2.5') |
| 1289 | + expect(body.provider).toBe('Fireworks') |
| 1290 | + expect(body.choices[0].message.content).toBe('fireworks response') |
| 1291 | + }) |
| 1292 | + }, |
| 1293 | + FETCH_PATH_TEST_TIMEOUT_MS, |
| 1294 | + ) |
| 1295 | + |
| 1296 | + it( |
| 1297 | + 'falls back to Fireworks when CanopyWave has no available workers for streaming requests', |
| 1298 | + async () => { |
| 1299 | + await withCanopyWaveApiKey(async () => { |
| 1300 | + const { fetch, fetchedBodies } = |
| 1301 | + createCanopyWaveNoWorkersThenFireworksFetch(true) |
| 1302 | + const response = await postCanopyWaveFallbackRequest({ |
| 1303 | + fetch, |
| 1304 | + stream: true, |
| 1305 | + }) |
| 1306 | + |
| 1307 | + expect(response.status).toBe(200) |
| 1308 | + expect(response.headers.get('Content-Type')).toBe('text/event-stream') |
| 1309 | + expectCanopyWaveThenFireworks(fetchedBodies) |
| 1310 | + }) |
| 1311 | + }, |
| 1312 | + FETCH_PATH_TEST_TIMEOUT_MS, |
| 1313 | + ) |
1161 | 1314 | }) |
1162 | 1315 |
|
1163 | 1316 | describe('Subscription limit enforcement', () => { |
|
0 commit comments