Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions restored-src/src/services/api/claude.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ import {
extractQuotaStatusFromHeaders,
} from '../claudeAiLimits.js'
import { getAPIContextManagement } from '../compact/apiMicrocompact.js'
import { normalizeMiniMaxVideoContent } from './minimaxVideoInput.js'

/* eslint-disable @typescript-eslint/no-require-imports */
const autoModeStateModule = feature('TRANSCRIPT_CLASSIFIER')
Expand Down Expand Up @@ -591,14 +592,15 @@ export function userMessageToMessageParam(
enablePromptCaching: boolean,
querySource?: QuerySource,
): MessageParam {
const content = normalizeMiniMaxVideoContent(message.message.content)
if (addCache) {
if (typeof message.message.content === 'string') {
if (typeof content === 'string') {
return {
role: 'user',
content: [
{
type: 'text',
text: message.message.content,
text: content,
...(enablePromptCaching && {
cache_control: getCacheControl({ querySource }),
}),
Expand All @@ -608,9 +610,9 @@ export function userMessageToMessageParam(
} else {
return {
role: 'user',
content: message.message.content.map((_, i) => ({
content: content.map((_, i) => ({
..._,
...(i === message.message.content.length - 1
...(i === content.length - 1
? enablePromptCaching
? { cache_control: getCacheControl({ querySource }) }
: {}
Expand All @@ -624,9 +626,7 @@ export function userMessageToMessageParam(
// to addCacheBreakpoints share the same array and each splices in duplicate cache_edits.
return {
role: 'user',
content: Array.isArray(message.message.content)
? [...message.message.content]
: message.message.content,
content: Array.isArray(content) ? [...content] : content,
}
}

Expand Down
79 changes: 79 additions & 0 deletions restored-src/src/services/api/minimaxVideoInput.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import assert from 'node:assert/strict'
import test from 'node:test'
import type { BetaContentBlockParam } from '@anthropic-ai/sdk/resources/beta/messages/messages.mjs'
import { normalizeMiniMaxVideoContent } from './minimaxVideoInput.ts'

function asContent(blocks: unknown[]): BetaContentBlockParam[] {
return blocks as BetaContentBlockParam[]
}

test('translates a video URL for the global Anthropic-compatible route', () => {
const content = asContent([
{ type: 'text', text: 'Describe this video.' },
{
type: 'video_url',
video_url: { url: 'https://example.com/demo.mp4' },
},
])

assert.deepEqual(
normalizeMiniMaxVideoContent(
content,
'https://api.minimax.io/anthropic/',
),
[
{ type: 'text', text: 'Describe this video.' },
{
type: 'video',
source: { type: 'url', url: 'https://example.com/demo.mp4' },
},
],
)
})

test('preserves video sampling options for the China route', () => {
const content = asContent([
{
type: 'video_url',
video_url: {
url: 'mm_file://video-file',
detail: 'high',
fps: 2,
max_long_side_pixel: 1080,
},
},
])

assert.deepEqual(
normalizeMiniMaxVideoContent(
content,
'https://api.minimaxi.com/anthropic',
),
[
{
type: 'video',
source: {
type: 'url',
url: 'mm_file://video-file',
detail: 'high',
fps: 2,
max_long_side_pixel: 1080,
},
},
],
)
})

test('leaves content unchanged for other routes', () => {
const content = asContent([
{
type: 'video_url',
video_url: { url: 'https://example.com/demo.mp4' },
},
])

assert.strictEqual(
normalizeMiniMaxVideoContent(content, 'https://example.com/anthropic'),
content,
)
})
64 changes: 64 additions & 0 deletions restored-src/src/services/api/minimaxVideoInput.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import type { BetaContentBlockParam } from '@anthropic-ai/sdk/resources/beta/messages/messages.mjs'

type MiniMaxVideoURLBlock = {
type: 'video_url'
video_url: {
url: string
detail?: 'low' | 'default' | 'high'
fps?: number
max_long_side_pixel?: number
}
}

const MINIMAX_ANTHROPIC_BASE_URLS = new Set([
'https://api.minimax.io/anthropic',
'https://api.minimaxi.com/anthropic',
])

function isMiniMaxAnthropicBaseUrl(baseUrl: string | undefined): boolean {
if (!baseUrl) return false

try {
const url = new URL(baseUrl)
const normalizedPath = url.pathname.replace(/\/+$/, '')
return MINIMAX_ANTHROPIC_BASE_URLS.has(`${url.origin}${normalizedPath}`)
} catch {
return false
}
}

export function normalizeMiniMaxVideoContent(
content: string | BetaContentBlockParam[],
baseUrl = process.env.ANTHROPIC_BASE_URL,
): string | BetaContentBlockParam[] {
if (typeof content === 'string' || !isMiniMaxAnthropicBaseUrl(baseUrl)) {
return content
}

let changed = false
const normalized = content.map(block => {
const candidate = block as unknown as MiniMaxVideoURLBlock
if (
candidate.type !== 'video_url' ||
typeof candidate.video_url?.url !== 'string' ||
candidate.video_url.url.length === 0
) {
return block
}

changed = true
const { url, detail, fps, max_long_side_pixel } = candidate.video_url
return {
type: 'video',
source: {
type: 'url',
url,
...(detail !== undefined && { detail }),
...(fps !== undefined && { fps }),
...(max_long_side_pixel !== undefined && { max_long_side_pixel }),
},
} as unknown as BetaContentBlockParam
})

return changed ? normalized : content
}