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
55 changes: 54 additions & 1 deletion src/crates/contracts/config-contracts/src/conversion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,11 @@ impl TryFrom<AIModelConfig> for AIConfig {
));
}

let custom_request_body = if let Some(body_str) = &other.custom_request_body {
let custom_request_body = if let Some(body_str) = other
.custom_request_body
.as_deref()
.filter(|body| !body.trim().is_empty())
{
match serde_json::from_str::<serde_json::Value>(body_str) {
Ok(value) => Some(value),
Err(e) => {
Expand Down Expand Up @@ -268,6 +272,55 @@ mod tests {
assert_eq!(config.model, "test-model");
}

#[test]
fn accepts_legacy_missing_null_and_blank_custom_request_bodies() {
for body in [
None,
Some(serde_json::Value::Null),
Some("".into()),
Some(" \n\t ".into()),
] {
let mut payload = serde_json::to_value(base_model_config()).unwrap();
payload
.as_object_mut()
.unwrap()
.remove("custom_request_body");
if let Some(body) = body {
payload["custom_request_body"] = body;
}
let model: AIModelConfig = serde_json::from_value(payload).unwrap();
let round_trip: AIModelConfig =
serde_json::from_value(serde_json::to_value(model).unwrap()).unwrap();
let config =
AIConfig::try_from(round_trip).expect("legacy config should remain usable");
assert_eq!(config.custom_request_body, None);
}
}

#[test]
fn preserves_non_empty_custom_request_body_and_mode() {
let mut model = base_model_config();
model.custom_request_body = Some(" \n{\"temperature\":0.2}\n ".to_string());
model.custom_request_body_mode = Some("trim".to_string());

let config = AIConfig::try_from(model).expect("conversion should succeed");
assert_eq!(
config.custom_request_body,
Some(serde_json::json!({"temperature": 0.2}))
);
assert_eq!(config.custom_request_body_mode.as_deref(), Some("trim"));
}

#[test]
fn ignores_malformed_non_empty_custom_request_body() {
let mut model = base_model_config();
model.custom_request_body = Some("{".to_string());

let config =
AIConfig::try_from(model).expect("existing parse fallback should remain usable");
assert_eq!(config.custom_request_body, None);
}

#[test]
fn derives_the_largest_output_tier_within_one_quarter_of_context() {
for (context_window, expected_max_tokens) in [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
import { ModelDiscoveryCoordinator } from '@/infrastructure/config/components/modelDiscoveryCoordinator';
import {
getModelEditorRequestSettings,
prepareModelEditorRequestSettingsForSave,
updateModelEditorRequestSettings,
type ModelEditorRequestSettings,
} from '@/infrastructure/config/components/modelEditorRequestSettings';
Expand Down Expand Up @@ -1995,7 +1996,7 @@ const ModelSettingsPage: React.FC = () => {
[PROVIDER_INSTANCE_METADATA_KEY]: providerInstanceId,
},
reasoning: draft.reasoning,
...draft.requestSettings,
...prepareModelEditorRequestSettingsForSave(draft.requestSettings),
auth: providerConfig.auth || { type: 'api_key' },
};
});
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,28 @@
import { describe, expect, it } from 'vitest';
import { getModelEditorRequestSettings, updateModelEditorRequestSettings } from './modelEditorRequestSettings';
import { getModelEditorRequestSettings, prepareModelEditorRequestSettingsForSave, updateModelEditorRequestSettings } from './modelEditorRequestSettings';

describe('model editor request drafts', () => {
it.each(['', ' \n\t '])('clears a previously saved custom body when the editor contains %j', body => {
const persisted = { custom_request_body: '{"temperature":0.2}', custom_request_body_mode: 'trim' as const };
const drafts = [{ key: 'model', requestSettings: getModelEditorRequestSettings(persisted) }];
const [edited] = updateModelEditorRequestSettings(drafts, { custom_request_body: body }, 'model');
const saved = { ...persisted, ...prepareModelEditorRequestSettingsForSave(edited.requestSettings) };
const reloaded = JSON.parse(JSON.stringify(saved));

expect(reloaded).not.toHaveProperty('custom_request_body');
expect(reloaded.custom_request_body_mode).toBe('trim');
expect(getModelEditorRequestSettings(reloaded).custom_request_body).toBeUndefined();
expect(edited.requestSettings.custom_request_body).toBe(body);
expect(persisted.custom_request_body).toBe('{"temperature":0.2}');
});

it('preserves non-empty custom JSON verbatim on save', () => {
const body = ' \n{"temperature":0.2}\n ';
expect(prepareModelEditorRequestSettingsForSave(getModelEditorRequestSettings({
custom_request_body: body,
})).custom_request_body).toBe(body);
});

it('accepts older configurations with no optional request settings and isolates header edits', () => {
expect(getModelEditorRequestSettings()).toMatchObject({
inline_think_in_text: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,13 @@ export function getModelEditorRequestSettings(config: Partial<AIModelConfig> = {
};
}

export function prepareModelEditorRequestSettingsForSave(settings: ModelEditorRequestSettings): ModelEditorRequestSettings {
return {
...settings,
custom_request_body: settings.custom_request_body?.trim() ? settings.custom_request_body : undefined,
};
}

export function updateModelEditorRequestSettings<T extends { key: string; requestSettings: ModelEditorRequestSettings }>(
drafts: T[],
updates: Partial<ModelEditorRequestSettings>,
Expand Down
Loading