Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -54,4 +54,4 @@ packages/backend/config/
research_and_plans/
packages/logs.txt
.vscode/mcp.json
.pi/readcache/
.pi/readcache/
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export const providerModels = pgTable(
pricingConfig: jsonb('pricing_config'),
modelType: modelTypeEnum('model_type'),
accessVia: jsonb('access_via'), // string[]
extraBody: jsonb('extra_body'), // Record<string, any>
sortOrder: integer('sort_order').notNull().default(0),
},
(table) => ({
Expand Down
1 change: 1 addition & 0 deletions packages/backend/drizzle/schema/sqlite/provider-models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export const providerModels = sqliteTable(
pricingConfig: text('pricing_config'), // JSON: pricing object
modelType: text('model_type'), // 'chat' | 'embeddings' | 'transcriptions' | 'speech' | 'image' | 'responses'
accessVia: text('access_via'), // JSON: string[]
extraBody: text('extra_body'), // JSON: Record<string, any>
sortOrder: integer('sort_order').notNull().default(0),
},
(table) => ({
Expand Down
1 change: 1 addition & 0 deletions packages/backend/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ const ModelProviderConfigSchema = z.object({
}),
access_via: z.array(z.string()).optional(),
type: z.enum(['chat', 'responses', 'embeddings', 'transcriptions', 'speech', 'image']).optional(),
extraBody: z.record(z.any()).optional(),
});

const OAuthProviderSchema = z.enum([
Expand Down
4 changes: 3 additions & 1 deletion packages/backend/src/db/config-repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ export class ConfigRepository {
estimateTokens: fromBool(config.estimateTokens === true),
useClaudeMasking: fromBool(config.useClaudeMasking === true),
headers: config.headers ? encryptJsonField(config.headers) : null,
extraBody: config.extraBody ? JSON.stringify(config.extraBody) : null,
extraBody: config.extraBody ? toJson(config.extraBody) : null,
quotaCheckerType: config.quota_checker?.type ?? null,
quotaCheckerId: config.quota_checker?.id ?? null,
quotaCheckerEnabled: fromBool(config.quota_checker?.enabled !== false),
Expand Down Expand Up @@ -273,6 +273,7 @@ export class ConfigRepository {
pricingConfig: toJson(cfg.pricing),
modelType: cfg.type ?? null,
accessVia: cfg.access_via ? toJson(cfg.access_via) : null,
extraBody: cfg.extraBody ? toJson(cfg.extraBody) : null,
sortOrder: idx,
}));
if (modelRows.length > 0) {
Expand Down Expand Up @@ -343,6 +344,7 @@ export class ConfigRepository {
pricing: parseJson(m.pricingConfig) ?? { source: 'simple', input: 0, output: 0 },
...(m.modelType ? { type: m.modelType } : {}),
...(m.accessVia ? { access_via: parseJson(m.accessVia) } : {}),
...(m.extraBody ? { extraBody: parseJson(m.extraBody) } : {}),
};
}
} else {
Expand Down
6 changes: 6 additions & 0 deletions packages/backend/src/services/dispatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1664,10 +1664,16 @@ export class Dispatcher {
providerPayload = await transformer.transformRequest(request);
}

// Merge provider-level extraBody first
if (route.config.extraBody) {
providerPayload = { ...providerPayload, ...route.config.extraBody };
}

// Then merge model-level extraBody (overrides provider-level)
if (route.modelConfig?.extraBody) {
providerPayload = { ...providerPayload, ...route.modelConfig.extraBody };
}

// Apply alias-level advanced behaviors (e.g. strip_adaptive_thinking)
if (route.canonicalModel) {
const aliasConfig = getConfig().models?.[route.canonicalModel];
Expand Down
140 changes: 140 additions & 0 deletions packages/frontend/src/pages/Providers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,7 @@ export const Providers = () => {
const [isApiBaseUrlsOpen, setIsApiBaseUrlsOpen] = useState(true);
const [isHeadersOpen, setIsHeadersOpen] = useState(false);
const [isExtraBodyOpen, setIsExtraBodyOpen] = useState(false);
const [isModelExtraBodyOpen, setIsModelExtraBodyOpen] = useState<Record<string, boolean>>({});
const [isAdvancedOpen, setIsAdvancedOpen] = useState(false);

// Fetch Models Modal state
Expand Down Expand Up @@ -762,6 +763,36 @@ export const Providers = () => {
setEditingProvider({ ...editingProvider, [field]: current });
};

// Model-level extraBody helpers
const addModelKV = (modelId: string) => {
const models = { ...(editingProvider.models as Record<string, any>) };
const current = models[modelId]?.extraBody || {};
models[modelId] = {
...models[modelId],
extraBody: { ...current, '': '' },
};
setEditingProvider({ ...editingProvider, models });
};

const updateModelKV = (modelId: string, oldKey: string, newKey: string, value: any) => {
const models = { ...(editingProvider.models as Record<string, any>) };
const current = { ...(models[modelId]?.extraBody || {}) };
if (oldKey !== newKey) {
delete current[oldKey];
}
current[newKey] = value;
models[modelId] = { ...models[modelId], extraBody: current };
setEditingProvider({ ...editingProvider, models });
};

const removeModelKV = (modelId: string, key: string) => {
const models = { ...(editingProvider.models as Record<string, any>) };
const current = { ...(models[modelId]?.extraBody || {}) };
delete current[key];
models[modelId] = { ...models[modelId], extraBody: current };
setEditingProvider({ ...editingProvider, models });
};

// Model Management
const addModel = () => {
const modelId = `model-${Date.now()}`;
Expand Down Expand Up @@ -3156,6 +3187,115 @@ export const Providers = () => {
</div>
</div>
)}

{/* Per-Model Extra Body Fields */}
<div
className="border border-border-glass rounded-md p-3 bg-bg-subtle"
style={{ marginTop: '12px' }}
>
<div
className="flex items-center gap-2 cursor-pointer"
style={{ minHeight: '38px' }}
onClick={() =>
setIsModelExtraBodyOpen({
...isModelExtraBodyOpen,
[mId]: !isModelExtraBodyOpen[mId],
})
}
>
{isModelExtraBodyOpen[mId] ? (
<ChevronDown size={14} />
) : (
<ChevronRight size={14} />
)}
<label
className="font-body text-[13px] font-medium text-text-secondary"
style={{ marginBottom: 0, flex: 1, cursor: 'pointer' }}
>
Extra Body Fields
</label>
<Badge
status="neutral"
style={{ fontSize: '10px', padding: '2px 8px' }}
>
{Object.keys(mCfg.extraBody || {}).length}
</Badge>
<Button
size="sm"
variant="secondary"
onClick={(e) => {
e.stopPropagation();
addModelKV(mId);
setIsModelExtraBodyOpen({
...isModelExtraBodyOpen,
[mId]: true,
});
}}
>
<Plus size={14} />
</Button>
</div>
{isModelExtraBodyOpen[mId] && (
<div
style={{
display: 'flex',
flexDirection: 'column',
gap: '4px',
padding: '8px',
borderTop: '1px solid var(--color-border-glass)',
background: 'var(--color-bg-deep)',
}}
>
{Object.entries(mCfg.extraBody || {}).length === 0 && (
<div className="font-body text-[11px] text-text-secondary italic">
No extra body fields configured.
</div>
)}
{Object.entries(mCfg.extraBody || {}).map(([key, val], idx) => (
<div key={idx} style={{ display: 'flex', gap: '6px' }}>
<Input
placeholder="Field Name"
value={key}
onChange={(e) =>
updateModelKV(mId, key, e.target.value, val)
}
style={{ flex: 1 }}
/>
<Input
placeholder="Value"
value={
typeof val === 'object'
? JSON.stringify(val)
: String(val)
}
onChange={(e) => {
const rawValue = e.target.value;
let parsedValue;
try {
parsedValue = JSON.parse(rawValue);
} catch {
parsedValue = rawValue;
}
updateModelKV(mId, key, key, parsedValue);
}}
style={{ flex: 1 }}
/>
<Button
variant="ghost"
size="sm"
onClick={() => removeModelKV(mId, key)}
style={{ padding: '4px' }}
>
<Trash2
size={14}
style={{ color: 'var(--color-danger)' }}
/>
</Button>
</div>
))}
</div>
)}
</div>
</div>
)}
</div>
Expand Down
Loading