From 074e4499a8111b18d9056ec28fbd0eec2e804eb9 Mon Sep 17 00:00:00 2001 From: Tushar Date: Wed, 1 Apr 2026 23:13:04 +0530 Subject: [PATCH 01/25] refactor(env): remove config fields from environment and update usage --- docs/reasoning-test.md | 116 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 docs/reasoning-test.md diff --git a/docs/reasoning-test.md b/docs/reasoning-test.md new file mode 100644 index 0000000000..2254a16ca5 --- /dev/null +++ b/docs/reasoning-test.md @@ -0,0 +1,116 @@ +# Reasoning Test Guide + +This guide explains how to manually verify that reasoning parameters are correctly serialized and sent to the provider API. + +## Prerequisites + +Build the application in debug mode before running any tests: + +```bash +cargo build +``` + +Optionally, inspect available CLI flags: + +```bash +target/debug/forge --help +``` + +## Steps + +### 1. Run Forge with debug request capture + +Set the following environment variables and run the binary with a simple prompt. The `FORGE_DEBUG_REQUESTS` variable writes the outgoing HTTP request body to the specified path inside the `.forge/` directory. + +```bash +FORGE_DEBUG_REQUESTS="forge.request.json" \ +FORGE_SESSION__PROVIDER_ID= \ +FORGE_SESSION__MODEL_ID= \ +target/debug/forge -p "Hello!" +``` + +Replace `` and `` with the provider and model you want to test (e.g. `anthropic` / `claude-opus-4-5`, `open_router` / `openai/o3`, etc.). + +### 2. Inspect the captured request + +After the command completes, a file is written to `.forge/forge.request.json`. Open it and verify that the correct reasoning parameters are present in the request body. + +#### OpenAI (Responses API) + +OpenAI o-series and GPT-5 models accept a `reasoning` object: + +```json +{ + "reasoning": { + "effort": "medium", + "summary": "auto" + } +} +``` + +- `effort`: `"low"` | `"medium"` | `"high"` — controls how many tokens the model spends on reasoning. +- `summary`: `"auto"` | `"concise"` | `"detailed"` — controls the reasoning summary returned in the response. When `exclude=true` is set in Forge, this maps to `"concise"`. + +Note: OpenAI o-series models do not return reasoning tokens in the response body. + +#### Anthropic + +**Newer models (Opus 4.6, Sonnet 4.6)** use the `output_config.effort` parameter: + +```json +{ + "output_config": { + "effort": "medium" + } +} +``` + +**Older models (Opus 4.5 and earlier)** use extended thinking with `budget_tokens`: + +```json +{ + "thinking": { + "type": "enabled", + "budget_tokens": 8000 + } +} +``` + +- `effort`: `"max"` | `"high"` (default) | `"medium"` | `"low"` — behavioral signal for thinking depth. `"max"` is only available on Opus 4.6; using it on other models returns an error. +- `budget_tokens`: integer — maximum number of thinking tokens; must be > 1024 and strictly less than the overall `max_tokens` to leave room for the final response. + +#### OpenRouter + +OpenRouter normalizes reasoning across providers using a unified `reasoning` object: + +```json +{ + "reasoning": { + "effort": "high", + "max_tokens": 2000, + "exclude": false + } +} +``` + +- `effort`: `"xhigh"` | `"high"` | `"medium"` | `"low"` | `"minimal"` | `"none"` — for OpenAI o-series and Grok models. Approximate token allocation: `xhigh` ≈ 95%, `high` ≈ 80%, `medium` ≈ 50%, `low` ≈ 20%, `minimal` ≈ 10% of `max_tokens`. +- `max_tokens`: integer (≥ 1024, ≤ 128 000) — for Anthropic and Gemini models; passed directly as `budget_tokens`. For Gemini 3 models it maps to `thinkingLevel` internally. +- `exclude`: boolean — when `true`, reasoning runs internally but is omitted from the response (`reasoning` field is absent). +- `enabled`: boolean — shorthand to activate reasoning at `"medium"` effort with no exclusions. + +Reasoning tokens appear in `choices[0].message.reasoning` (plain text) and in the structured `choices[0].message.reasoning_details` array. + +--- + +The `ReasoningConfig` fields in Forge that drive all of the above are: + +- `enabled` — activates reasoning at medium effort (supported by OpenRouter, Anthropic, and Forge) +- `effort` — explicit effort level: `low`, `medium`, or `high` (supported by OpenRouter and Forge) +- `max_tokens` — token budget for thinking; must be > 1024 (supported by OpenRouter, Anthropic, and Forge) +- `exclude` — hides the reasoning trace from the response (supported by OpenRouter and Forge) + +## References + +- [OpenAI Reasoning](https://developers.openai.com/api/docs/guides/reasoning) +- [Anthropic Extended Thinking](https://platform.claude.com/docs/en/build-with-claude/effort) +- [OpenRouter Reasoning Tokens](https://openrouter.ai/docs/guides/best-practices/reasoning-tokens) From dba801266680a69522f4dc8156cdb295aa57785a Mon Sep 17 00:00:00 2001 From: Tushar Date: Wed, 1 Apr 2026 23:32:11 +0530 Subject: [PATCH 02/25] refactor(reasoning): clarify API instructions and update examples --- docs/reasoning-test.md | 42 ++++++++++++++++++++++++++++++++++-------- 1 file changed, 34 insertions(+), 8 deletions(-) diff --git a/docs/reasoning-test.md b/docs/reasoning-test.md index 2254a16ca5..ac695f20b8 100644 --- a/docs/reasoning-test.md +++ b/docs/reasoning-test.md @@ -29,15 +29,42 @@ FORGE_SESSION__MODEL_ID= \ target/debug/forge -p "Hello!" ``` -Replace `` and `` with the provider and model you want to test (e.g. `anthropic` / `claude-opus-4-5`, `open_router` / `openai/o3`, etc.). +Replace `` and `` with the provider and model you want to test. To see available providers: + +```bash +target/debug/forge provider list +``` ### 2. Inspect the captured request After the command completes, a file is written to `.forge/forge.request.json`. Open it and verify that the correct reasoning parameters are present in the request body. -#### OpenAI (Responses API) +#### OpenAI — Chat Completions API -OpenAI o-series and GPT-5 models accept a `reasoning` object: +The Chat Completions API (`POST /v1/chat/completions`) uses a top-level `reasoning_effort` string field: + +```json +{ + "model": "gpt-5.1", + "reasoning_effort": "medium", + "messages": [...] +} +``` + +- `reasoning_effort`: `"none"` | `"minimal"` | `"low"` | `"medium"` | `"high"` | `"xhigh"` — constrains how many tokens the model spends on reasoning. Reducing it produces faster responses at lower cost. + +Model-specific defaults and constraints: + +- `gpt-5.1` defaults to `"none"` (no reasoning); supports `"none"`, `"low"`, `"medium"`, `"high"`. +- Models before `gpt-5.1` default to `"medium"` and do not support `"none"`. +- `gpt-5-pro` defaults to and only supports `"high"`. +- `"xhigh"` is supported for all models after `gpt-5.1-codex-max`. + +Note: OpenAI does not return reasoning tokens in the response body. + +#### OpenAI — Responses API + +The Responses API uses a nested `reasoning` object instead: ```json { @@ -48,10 +75,8 @@ OpenAI o-series and GPT-5 models accept a `reasoning` object: } ``` -- `effort`: `"low"` | `"medium"` | `"high"` — controls how many tokens the model spends on reasoning. -- `summary`: `"auto"` | `"concise"` | `"detailed"` — controls the reasoning summary returned in the response. When `exclude=true` is set in Forge, this maps to `"concise"`. - -Note: OpenAI o-series models do not return reasoning tokens in the response body. +- `effort`: `"low"` | `"medium"` | `"high"` — controls reasoning depth. +- `summary`: `"auto"` | `"concise"` | `"detailed"` — controls the reasoning summary in the response. When `exclude=true` is set in Forge, this maps to `"concise"`. #### Anthropic @@ -111,6 +136,7 @@ The `ReasoningConfig` fields in Forge that drive all of the above are: ## References -- [OpenAI Reasoning](https://developers.openai.com/api/docs/guides/reasoning) +- [OpenAI Reasoning guide](https://developers.openai.com/api/docs/guides/reasoning) +- [OpenAI Chat Completions API reference](https://developers.openai.com/api/reference/resources/chat/subresources/completions/methods/create) - [Anthropic Extended Thinking](https://platform.claude.com/docs/en/build-with-claude/effort) - [OpenRouter Reasoning Tokens](https://openrouter.ai/docs/guides/best-practices/reasoning-tokens) From 5a5ad31261b76ebf36565e9a829916789c838845 Mon Sep 17 00:00:00 2001 From: Tushar Date: Wed, 1 Apr 2026 23:42:31 +0530 Subject: [PATCH 03/25] refactor(reasoning): clarify API instructions and update examples --- docs/reasoning-test.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/docs/reasoning-test.md b/docs/reasoning-test.md index ac695f20b8..fa1c797c8a 100644 --- a/docs/reasoning-test.md +++ b/docs/reasoning-test.md @@ -134,6 +134,22 @@ The `ReasoningConfig` fields in Forge that drive all of the above are: - `max_tokens` — token budget for thinking; must be > 1024 (supported by OpenRouter, Anthropic, and Forge) - `exclude` — hides the reasoning trace from the response (supported by OpenRouter and Forge) +## Test Matrix + +Use these combinations to exercise reasoning across providers and models. For each row, set the listed `ReasoningConfig` fields in your agent definition (or via the forge config), then verify the JSON field appears in `.forge/forge.request.json`. + +| Provider | Model | `ReasoningConfig` fields | JSON field in request | Valid effort / token values | +| ------------- | ---------------------------- | ------------------------------------ | ------------------------------------------ | -------------------------------------------------------- | +| `open_router` | `openai/o4-mini` | `effort: high` | `reasoning.effort` | `none` · `minimal` · `low` · `medium` · `high` · `xhigh` | +| `open_router` | `anthropic/claude-opus-4-5` | `max_tokens: 4000` | `reasoning.max_tokens` | integer ≥ 1024 | +| `anthropic` | `claude-opus-4-6` | `effort: medium` | `output_config.effort` | `low` · `medium` · `high` · `max`¹ | +| `anthropic` | `claude-3-7-sonnet-20250219` | `enabled: true` + `max_tokens: 8000` | `thinking.type` + `thinking.budget_tokens` | integer ≥ 1024 | + +**Notes:** + +1. `max` effort is only available on `claude-opus-4-6`; using it on any other model returns an API error. +2. The `openai` provider strips the `reasoning` field before sending. Use `open_router` with an OpenAI model (e.g. `openai/o4-mini`) to test OpenAI reasoning. + ## References - [OpenAI Reasoning guide](https://developers.openai.com/api/docs/guides/reasoning) From 3cf582272918d0d327073f50aebbb94e839156b4 Mon Sep 17 00:00:00 2001 From: Tushar Date: Thu, 2 Apr 2026 00:29:42 +0530 Subject: [PATCH 04/25] refactor(reasoning): clarify API instructions and update examples --- docs/reasoning-test.md | 12 ++++++++- forge.schema.json | 58 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 1 deletion(-) diff --git a/docs/reasoning-test.md b/docs/reasoning-test.md index fa1c797c8a..7644185dd6 100644 --- a/docs/reasoning-test.md +++ b/docs/reasoning-test.md @@ -26,10 +26,20 @@ Set the following environment variables and run the binary with a simple prompt. FORGE_DEBUG_REQUESTS="forge.request.json" \ FORGE_SESSION__PROVIDER_ID= \ FORGE_SESSION__MODEL_ID= \ +FORGE_REASONING__EFFORT= \ target/debug/forge -p "Hello!" ``` -Replace `` and `` with the provider and model you want to test. To see available providers: +The `FORGE_REASONING__*` variables map directly to `ReasoningConfig` fields and apply globally to all agents: + +| Variable | Values | +| ----------------------------- | --------------------------------- | +| `FORGE_REASONING__EFFORT` | `low` · `medium` · `high` · `max` | +| `FORGE_REASONING__ENABLED` | `true` · `false` | +| `FORGE_REASONING__MAX_TOKENS` | integer ≥ 1024 | +| `FORGE_REASONING__EXCLUDE` | `true` · `false` | + +Replace ``, ``, and `` with the values you want to test. To see available providers: ```bash target/debug/forge provider list diff --git a/forge.schema.json b/forge.schema.json index 80ef5e8850..96d5563bd4 100644 --- a/forge.schema.json +++ b/forge.schema.json @@ -206,6 +206,17 @@ "default": 0, "minimum": 0 }, + "reasoning": { + "description": "Reasoning settings applied to all agents; individual agent settings\ntake priority over this global default via merge semantics.", + "anyOf": [ + { + "$ref": "#/$defs/ReasoningConfig" + }, + { + "type": "null" + } + ] + }, "restricted": { "description": "Whether restricted mode is active; when enabled, tool execution requires\nexplicit permission grants.", "type": "boolean", @@ -395,6 +406,14 @@ } } }, + "Effort": { + "type": "string", + "enum": [ + "high", + "medium", + "low" + ] + }, "HttpConfig": { "description": "HTTP client configuration.", "type": "object", @@ -524,6 +543,45 @@ } } }, + "ReasoningConfig": { + "type": "object", + "properties": { + "effort": { + "description": "Controls the effort level of the agent's reasoning\nsupported by openrouter and forge provider", + "anyOf": [ + { + "$ref": "#/$defs/Effort" + }, + { + "type": "null" + } + ] + }, + "enabled": { + "description": "Enables reasoning at the \"medium\" effort level with no exclusions.\nsupported by openrouter, anthropic and forge provider", + "type": [ + "boolean", + "null" + ] + }, + "exclude": { + "description": "Model thinks deeply, but the reasoning is hidden from you.\nsupported by openrouter and forge provider", + "type": [ + "boolean", + "null" + ] + }, + "max_tokens": { + "description": "Controls how many tokens the model can spend thinking.\nsupported by openrouter, anthropic and forge provider\nshould be greater then 1024 but less than overall max_tokens", + "type": [ + "integer", + "null" + ], + "format": "uint", + "minimum": 0 + } + } + }, "RetryConfig": { "description": "Configuration for retry mechanism.", "type": "object", From ad3e8678520fec645fd7ff31e3f11554a4be7755 Mon Sep 17 00:00:00 2001 From: Tushar Date: Thu, 2 Apr 2026 00:37:05 +0530 Subject: [PATCH 05/25] feat(reasoning): add configurable reasoning settings to all agents --- Cargo.lock | 1 + crates/forge_config/.forge.toml | 4 ++ crates/forge_config/Cargo.toml | 1 + crates/forge_config/src/config.rs | 10 +++- crates/forge_config/src/lib.rs | 2 + crates/forge_config/src/reasoning.rs | 89 ++++++++++++++++++++++++++++ 6 files changed, 106 insertions(+), 1 deletion(-) create mode 100644 crates/forge_config/src/reasoning.rs diff --git a/Cargo.lock b/Cargo.lock index 4b483ce256..bc51720766 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1950,6 +1950,7 @@ dependencies = [ "schemars 1.2.1", "serde", "serde_json", + "strum_macros 0.28.0", "thiserror 2.0.18", "tokio", "toml_edit", diff --git a/crates/forge_config/.forge.toml b/crates/forge_config/.forge.toml index fa2331e690..f324b6c9ab 100644 --- a/crates/forge_config/.forge.toml +++ b/crates/forge_config/.forge.toml @@ -59,3 +59,7 @@ token_threshold = 100000 [updates] auto_update = true frequency = "daily" + +[reasoning] +enabled = true +effort = "medium" diff --git a/crates/forge_config/Cargo.toml b/crates/forge_config/Cargo.toml index 038d12f524..96c58b829a 100644 --- a/crates/forge_config/Cargo.toml +++ b/crates/forge_config/Cargo.toml @@ -16,6 +16,7 @@ toml_edit = { workspace = true } url.workspace = true fake = { version = "5.1.0", features = ["derive"] } schemars.workspace = true +strum_macros.workspace = true tracing.workspace = true [dev-dependencies] diff --git a/crates/forge_config/src/config.rs b/crates/forge_config/src/config.rs index 6c550e4962..adea3abeb4 100644 --- a/crates/forge_config/src/config.rs +++ b/crates/forge_config/src/config.rs @@ -7,7 +7,10 @@ use serde::{Deserialize, Serialize}; use crate::reader::ConfigReader; use crate::writer::ConfigWriter; -use crate::{AutoDumpFormat, Compact, Decimal, HttpConfig, ModelConfig, RetryConfig, Update}; +use crate::{ + AutoDumpFormat, Compact, Decimal, HttpConfig, ModelConfig, ReasoningConfig, RetryConfig, + Update, +}; /// Top-level Forge configuration merged from all sources (defaults, file, /// environment). @@ -159,6 +162,11 @@ pub struct ForgeConfig { /// all tool calls are disabled. #[serde(default)] pub tool_supported: bool, + + /// Reasoning configuration applied to all agents; controls effort level, + /// token budget, and visibility of the model's thinking process. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub reasoning: Option, } impl ForgeConfig { diff --git a/crates/forge_config/src/lib.rs b/crates/forge_config/src/lib.rs index 6bcbdaa147..cc253277e4 100644 --- a/crates/forge_config/src/lib.rs +++ b/crates/forge_config/src/lib.rs @@ -8,6 +8,7 @@ mod legacy; mod model; mod percentage; mod reader; +mod reasoning; mod retry; mod writer; @@ -20,6 +21,7 @@ pub use http::*; pub use model::*; pub use percentage::*; pub use reader::*; +pub use reasoning::*; pub use retry::*; pub use writer::*; diff --git a/crates/forge_config/src/reasoning.rs b/crates/forge_config/src/reasoning.rs new file mode 100644 index 0000000000..23792de963 --- /dev/null +++ b/crates/forge_config/src/reasoning.rs @@ -0,0 +1,89 @@ +use derive_setters::Setters; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use strum_macros::Display as StrumDisplay; + +/// Controls the reasoning behaviour of a model, including effort level, token +/// budget, and visibility of the thinking process. +#[derive( + Default, Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema, fake::Dummy, Setters, +)] +#[serde(rename_all = "snake_case")] +#[setters(strip_option)] +pub struct ReasoningConfig { + /// Controls the effort level of the model's reasoning. + /// Supported by openrouter and forge provider. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub effort: Option, + + /// Controls how many tokens the model can spend thinking. + /// Should be greater than 1024 but less than the overall max_tokens. + /// Supported by openrouter, anthropic, and forge provider. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub max_tokens: Option, + + /// When true, the model thinks deeply but the reasoning is hidden from the + /// caller. Supported by openrouter and forge provider. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub exclude: Option, + + /// Enables reasoning at the "medium" effort level with no exclusions. + /// Supported by openrouter, anthropic, and forge provider. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub enabled: Option, +} + +/// Effort level for model reasoning. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema, fake::Dummy, StrumDisplay)] +#[serde(rename_all = "lowercase")] +#[strum(serialize_all = "lowercase")] +pub enum Effort { + High, + Medium, + Low, +} + +/// Converts a thinking budget (token count) to an [`Effort`] level. +/// +/// - 0–1024 → Low +/// - 1025–8192 → Medium +/// - 8193+ → High +impl From for Effort { + fn from(budget: usize) -> Self { + if budget <= 1024 { + Effort::Low + } else if budget <= 8192 { + Effort::Medium + } else { + Effort::High + } + } +} + +#[cfg(test)] +mod tests { + use pretty_assertions::assert_eq; + + use super::*; + + #[test] + fn test_effort_from_budget_low() { + assert_eq!(Effort::from(0), Effort::Low); + assert_eq!(Effort::from(1), Effort::Low); + assert_eq!(Effort::from(1024), Effort::Low); + } + + #[test] + fn test_effort_from_budget_medium() { + assert_eq!(Effort::from(1025), Effort::Medium); + assert_eq!(Effort::from(5000), Effort::Medium); + assert_eq!(Effort::from(8192), Effort::Medium); + } + + #[test] + fn test_effort_from_budget_high() { + assert_eq!(Effort::from(8193), Effort::High); + assert_eq!(Effort::from(10000), Effort::High); + assert_eq!(Effort::from(100000), Effort::High); + } +} From 46205dc699ee5d1eca7effc8dde03d9ef9cd546b Mon Sep 17 00:00:00 2001 From: Tushar Date: Thu, 2 Apr 2026 00:41:41 +0530 Subject: [PATCH 06/25] refactor(agents): merge reasoning config with agent settings prioritizing agent-level fields --- crates/forge_app/src/agent.rs | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/crates/forge_app/src/agent.rs b/crates/forge_app/src/agent.rs index 39bd10b78a..5e429cea00 100644 --- a/crates/forge_app/src/agent.rs +++ b/crates/forge_app/src/agent.rs @@ -2,8 +2,9 @@ use std::sync::Arc; use forge_config::ForgeConfig; use forge_domain::{ - Agent, ChatCompletionMessage, Compact, Context, Conversation, MaxTokens, ModelId, ProviderId, - ResultStream, Temperature, ToolCallContext, ToolCallFull, ToolResult, TopK, TopP, + Agent, ChatCompletionMessage, Compact, Context, Conversation, Effort, MaxTokens, ModelId, + ProviderId, ReasoningConfig, ResultStream, Temperature, ToolCallContext, ToolCallFull, + ToolResult, TopK, TopP, }; use merge::Merge; @@ -142,6 +143,25 @@ impl AgentExt for Agent { agent.compact = merged_compact; } + // Apply workflow reasoning configuration to agents. + // Config provides the base; agent-level settings take priority via merge. + if let Some(ref config_reasoning) = config.reasoning { + use forge_config::Effort as ConfigEffort; + let mut base = ReasoningConfig { + effort: config_reasoning.effort.as_ref().map(|e| match e { + ConfigEffort::High => Effort::High, + ConfigEffort::Medium => Effort::Medium, + ConfigEffort::Low => Effort::Low, + }), + max_tokens: config_reasoning.max_tokens, + exclude: config_reasoning.exclude, + enabled: config_reasoning.enabled, + }; + // Merge agent's reasoning on top so agent-level fields take priority. + base.merge(agent.reasoning.clone().unwrap_or_default()); + agent.reasoning = Some(base); + } + agent } } From a50da3bdad8ea4fe1055d70b0416e8a79994ad13 Mon Sep 17 00:00:00 2001 From: Tushar Date: Thu, 2 Apr 2026 00:44:43 +0530 Subject: [PATCH 07/25] refactor(agents): improve reasoning config merging logic with agent-level priority --- crates/forge_app/src/agent.rs | 75 ++++++++++++++++++++++++++++++++--- 1 file changed, 70 insertions(+), 5 deletions(-) diff --git a/crates/forge_app/src/agent.rs b/crates/forge_app/src/agent.rs index 5e429cea00..1edcce1619 100644 --- a/crates/forge_app/src/agent.rs +++ b/crates/forge_app/src/agent.rs @@ -144,10 +144,10 @@ impl AgentExt for Agent { } // Apply workflow reasoning configuration to agents. - // Config provides the base; agent-level settings take priority via merge. + // Agent-level fields take priority; config fills in any unset fields. if let Some(ref config_reasoning) = config.reasoning { use forge_config::Effort as ConfigEffort; - let mut base = ReasoningConfig { + let config_as_domain = ReasoningConfig { effort: config_reasoning.effort.as_ref().map(|e| match e { ConfigEffort::High => Effort::High, ConfigEffort::Medium => Effort::Medium, @@ -157,11 +157,76 @@ impl AgentExt for Agent { exclude: config_reasoning.exclude, enabled: config_reasoning.enabled, }; - // Merge agent's reasoning on top so agent-level fields take priority. - base.merge(agent.reasoning.clone().unwrap_or_default()); - agent.reasoning = Some(base); + // Start from the agent's own settings and fill unset fields from config. + let mut merged = agent.reasoning.clone().unwrap_or_default(); + merged.merge(config_as_domain); + agent.reasoning = Some(merged); } agent } } + +#[cfg(test)] +mod tests { + use forge_config::{Effort as ConfigEffort, ReasoningConfig as ConfigReasoningConfig}; + use forge_domain::{AgentId, Effort, ModelId, ProviderId, ReasoningConfig}; + use pretty_assertions::assert_eq; + + use super::*; + + fn fixture_agent() -> Agent { + Agent::new( + AgentId::new("test"), + ProviderId::ANTHROPIC, + ModelId::new("claude-3-5-sonnet-20241022"), + ) + } + + /// When the agent has no reasoning config, the config's reasoning is applied + /// in full. + #[test] + fn test_reasoning_applied_from_config_when_agent_has_none() { + let config = ForgeConfig::default().reasoning( + ConfigReasoningConfig::default() + .enabled(true) + .effort(ConfigEffort::Medium), + ); + + let actual = fixture_agent().apply_config(&config).reasoning; + + let expected = Some( + ReasoningConfig::default() + .enabled(true) + .effort(Effort::Medium), + ); + + assert_eq!(actual, expected); + } + + /// When the agent already has reasoning fields set, those fields take + /// priority; config only fills in fields the agent left unset. + #[test] + fn test_reasoning_agent_fields_take_priority_over_config() { + let config = ForgeConfig::default().reasoning( + ConfigReasoningConfig::default() + .enabled(true) + .effort(ConfigEffort::Low) + .max_tokens(1024_usize), + ); + + // Agent overrides effort but leaves enabled and max_tokens unset. + let agent = fixture_agent().reasoning(ReasoningConfig::default().effort(Effort::High)); + + let actual = agent.apply_config(&config).reasoning; + + let expected = Some( + ReasoningConfig::default() + .effort(Effort::High) // agent's value wins + .enabled(true) // filled in from config + .max_tokens(1024_usize), // filled in from config + ); + + assert_eq!(actual, expected); + } +} From b240bb72b3650e26e9a8497cbe7f05c471cf73c6 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Wed, 1 Apr 2026 19:17:10 +0000 Subject: [PATCH 08/25] [autofix.ci] apply automated fixes --- crates/forge_app/src/agent.rs | 4 ++-- crates/forge_config/src/config.rs | 3 +-- crates/forge_config/src/reasoning.rs | 4 +++- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/crates/forge_app/src/agent.rs b/crates/forge_app/src/agent.rs index 1edcce1619..d4cebfb8d9 100644 --- a/crates/forge_app/src/agent.rs +++ b/crates/forge_app/src/agent.rs @@ -183,8 +183,8 @@ mod tests { ) } - /// When the agent has no reasoning config, the config's reasoning is applied - /// in full. + /// When the agent has no reasoning config, the config's reasoning is + /// applied in full. #[test] fn test_reasoning_applied_from_config_when_agent_has_none() { let config = ForgeConfig::default().reasoning( diff --git a/crates/forge_config/src/config.rs b/crates/forge_config/src/config.rs index adea3abeb4..d152050a8d 100644 --- a/crates/forge_config/src/config.rs +++ b/crates/forge_config/src/config.rs @@ -8,8 +8,7 @@ use serde::{Deserialize, Serialize}; use crate::reader::ConfigReader; use crate::writer::ConfigWriter; use crate::{ - AutoDumpFormat, Compact, Decimal, HttpConfig, ModelConfig, ReasoningConfig, RetryConfig, - Update, + AutoDumpFormat, Compact, Decimal, HttpConfig, ModelConfig, ReasoningConfig, RetryConfig, Update, }; /// Top-level Forge configuration merged from all sources (defaults, file, diff --git a/crates/forge_config/src/reasoning.rs b/crates/forge_config/src/reasoning.rs index 23792de963..2187299bd3 100644 --- a/crates/forge_config/src/reasoning.rs +++ b/crates/forge_config/src/reasoning.rs @@ -34,7 +34,9 @@ pub struct ReasoningConfig { } /// Effort level for model reasoning. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema, fake::Dummy, StrumDisplay)] +#[derive( + Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema, fake::Dummy, StrumDisplay, +)] #[serde(rename_all = "lowercase")] #[strum(serialize_all = "lowercase")] pub enum Effort { From 2a1e63784650a807d06e7159cbbfa41c13ce14da Mon Sep 17 00:00:00 2001 From: Tushar Date: Thu, 2 Apr 2026 00:51:24 +0530 Subject: [PATCH 09/25] refactor(config): expand Effort enum and token budget mapping --- crates/forge_app/src/agent.rs | 8 +++- crates/forge_config/src/reasoning.rs | 68 +++++++++++++++++++++----- crates/forge_domain/src/agent.rs | 71 +++++++++++++++++++++++----- 3 files changed, 122 insertions(+), 25 deletions(-) diff --git a/crates/forge_app/src/agent.rs b/crates/forge_app/src/agent.rs index d4cebfb8d9..30ce87198a 100644 --- a/crates/forge_app/src/agent.rs +++ b/crates/forge_app/src/agent.rs @@ -149,9 +149,13 @@ impl AgentExt for Agent { use forge_config::Effort as ConfigEffort; let config_as_domain = ReasoningConfig { effort: config_reasoning.effort.as_ref().map(|e| match e { - ConfigEffort::High => Effort::High, - ConfigEffort::Medium => Effort::Medium, + ConfigEffort::None => Effort::None, + ConfigEffort::Minimal => Effort::Minimal, ConfigEffort::Low => Effort::Low, + ConfigEffort::Medium => Effort::Medium, + ConfigEffort::High => Effort::High, + ConfigEffort::XHigh => Effort::XHigh, + ConfigEffort::Max => Effort::Max, }), max_tokens: config_reasoning.max_tokens, exclude: config_reasoning.exclude, diff --git a/crates/forge_config/src/reasoning.rs b/crates/forge_config/src/reasoning.rs index 2187299bd3..03f701fc25 100644 --- a/crates/forge_config/src/reasoning.rs +++ b/crates/forge_config/src/reasoning.rs @@ -40,24 +40,47 @@ pub struct ReasoningConfig { #[serde(rename_all = "lowercase")] #[strum(serialize_all = "lowercase")] pub enum Effort { - High, - Medium, + /// No reasoning; skips the thinking step entirely. + None, + /// Minimal reasoning; fastest and cheapest. + Minimal, + /// Low reasoning effort. Low, + /// Medium reasoning effort; the default for most providers. + Medium, + /// High reasoning effort. + High, + /// Extra-high reasoning effort (OpenAI / OpenRouter). + XHigh, + /// Maximum reasoning effort; only available on select Anthropic models. + Max, } -/// Converts a thinking budget (token count) to an [`Effort`] level. +/// Converts a thinking budget (token count) to the closest [`Effort`] level. /// -/// - 0–1024 → Low +/// - 0 → None +/// - 1–512 → Minimal +/// - 513–1024 → Low /// - 1025–8192 → Medium -/// - 8193+ → High +/// - 8193–32768 → High +/// - 32769–65536 → XHigh +/// - 65537+ → Max impl From for Effort { fn from(budget: usize) -> Self { - if budget <= 1024 { + if budget == 0 { + Effort::None + } else if budget <= 512 { + Effort::Minimal + } else if budget <= 1024 { Effort::Low } else if budget <= 8192 { Effort::Medium - } else { + } else if budget <= 32768 { Effort::High + } else if budget <= 65536 { + Effort::XHigh + } else { + Effort::Max } } } @@ -68,10 +91,20 @@ mod tests { use super::*; + #[test] + fn test_effort_from_budget_none() { + assert_eq!(Effort::from(0), Effort::None); + } + + #[test] + fn test_effort_from_budget_minimal() { + assert_eq!(Effort::from(1), Effort::Minimal); + assert_eq!(Effort::from(512), Effort::Minimal); + } + #[test] fn test_effort_from_budget_low() { - assert_eq!(Effort::from(0), Effort::Low); - assert_eq!(Effort::from(1), Effort::Low); + assert_eq!(Effort::from(513), Effort::Low); assert_eq!(Effort::from(1024), Effort::Low); } @@ -85,7 +118,20 @@ mod tests { #[test] fn test_effort_from_budget_high() { assert_eq!(Effort::from(8193), Effort::High); - assert_eq!(Effort::from(10000), Effort::High); - assert_eq!(Effort::from(100000), Effort::High); + assert_eq!(Effort::from(20000), Effort::High); + assert_eq!(Effort::from(32768), Effort::High); + } + + #[test] + fn test_effort_from_budget_xhigh() { + assert_eq!(Effort::from(32769), Effort::XHigh); + assert_eq!(Effort::from(50000), Effort::XHigh); + assert_eq!(Effort::from(65536), Effort::XHigh); + } + + #[test] + fn test_effort_from_budget_max() { + assert_eq!(Effort::from(65537), Effort::Max); + assert_eq!(Effort::from(100000), Effort::Max); } } diff --git a/crates/forge_domain/src/agent.rs b/crates/forge_domain/src/agent.rs index 2359a4626d..2165fd033a 100644 --- a/crates/forge_domain/src/agent.rs +++ b/crates/forge_domain/src/agent.rs @@ -75,23 +75,47 @@ pub struct ReasoningConfig { #[serde(rename_all = "lowercase")] #[strum(serialize_all = "lowercase")] pub enum Effort { - High, - Medium, + /// No reasoning; skips the thinking step entirely. + None, + /// Minimal reasoning; fastest and cheapest. + Minimal, + /// Low reasoning effort. Low, + /// Medium reasoning effort; the default for most providers. + Medium, + /// High reasoning effort. + High, + /// Extra-high reasoning effort (OpenAI / OpenRouter). + XHigh, + /// Maximum reasoning effort; only available on select Anthropic models. + Max, } -/// Converts a thinking budget (max_tokens) to Effort -/// - 0-1024 → Low -/// - 1025-8192 → Medium -/// - 8193+ → High +/// Converts a thinking budget (max_tokens) to the closest [`Effort`] level. +/// +/// - 0 → None +/// - 1–512 → Minimal +/// - 513–1024 → Low +/// - 1025–8192 → Medium +/// - 8193–32768 → High +/// - 32769–65536 → XHigh +/// - 65537+ → Max impl From for Effort { fn from(budget: usize) -> Self { - if budget <= 1024 { + if budget == 0 { + Effort::None + } else if budget <= 512 { + Effort::Minimal + } else if budget <= 1024 { Effort::Low } else if budget <= 8192 { Effort::Medium - } else { + } else if budget <= 32768 { Effort::High + } else if budget <= 65536 { + Effort::XHigh + } else { + Effort::Max } } } @@ -245,10 +269,20 @@ impl From for ToolDefinition { mod tests { use super::*; + #[test] + fn test_effort_from_budget_none() { + assert_eq!(Effort::from(0), Effort::None); + } + + #[test] + fn test_effort_from_budget_minimal() { + assert_eq!(Effort::from(1), Effort::Minimal); + assert_eq!(Effort::from(512), Effort::Minimal); + } + #[test] fn test_effort_from_budget_low() { - assert_eq!(Effort::from(0), Effort::Low); - assert_eq!(Effort::from(1), Effort::Low); + assert_eq!(Effort::from(513), Effort::Low); assert_eq!(Effort::from(1024), Effort::Low); } @@ -262,7 +296,20 @@ mod tests { #[test] fn test_effort_from_budget_high() { assert_eq!(Effort::from(8193), Effort::High); - assert_eq!(Effort::from(10000), Effort::High); - assert_eq!(Effort::from(100000), Effort::High); + assert_eq!(Effort::from(20000), Effort::High); + assert_eq!(Effort::from(32768), Effort::High); + } + + #[test] + fn test_effort_from_budget_xhigh() { + assert_eq!(Effort::from(32769), Effort::XHigh); + assert_eq!(Effort::from(50000), Effort::XHigh); + assert_eq!(Effort::from(65536), Effort::XHigh); + } + + #[test] + fn test_effort_from_budget_max() { + assert_eq!(Effort::from(65537), Effort::Max); + assert_eq!(Effort::from(100000), Effort::Max); } } From e1552fb3867e9d35b4f2fccff822735856028123 Mon Sep 17 00:00:00 2001 From: Tushar Date: Thu, 2 Apr 2026 00:56:30 +0530 Subject: [PATCH 10/25] refactor(conversation_record): expand Effort enum and update conversions --- .../src/conversation/conversation_record.rs | 24 ++++++++++++++----- .../src/provider/openai_responses/request.rs | 8 +++++-- 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/crates/forge_repo/src/conversation/conversation_record.rs b/crates/forge_repo/src/conversation/conversation_record.rs index c8d0a263b4..bee939e30f 100644 --- a/crates/forge_repo/src/conversation/conversation_record.rs +++ b/crates/forge_repo/src/conversation/conversation_record.rs @@ -647,17 +647,25 @@ impl From for forge_domain::ToolChoice { #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "lowercase")] pub(super) enum EffortRecord { - High, - Medium, + None, + Minimal, Low, + Medium, + High, + XHigh, + Max, } impl From<&forge_domain::Effort> for EffortRecord { fn from(effort: &forge_domain::Effort) -> Self { match effort { - forge_domain::Effort::High => Self::High, - forge_domain::Effort::Medium => Self::Medium, + forge_domain::Effort::None => Self::None, + forge_domain::Effort::Minimal => Self::Minimal, forge_domain::Effort::Low => Self::Low, + forge_domain::Effort::Medium => Self::Medium, + forge_domain::Effort::High => Self::High, + forge_domain::Effort::XHigh => Self::XHigh, + forge_domain::Effort::Max => Self::Max, } } } @@ -665,9 +673,13 @@ impl From<&forge_domain::Effort> for EffortRecord { impl From for forge_domain::Effort { fn from(record: EffortRecord) -> Self { match record { - EffortRecord::High => Self::High, - EffortRecord::Medium => Self::Medium, + EffortRecord::None => Self::None, + EffortRecord::Minimal => Self::Minimal, EffortRecord::Low => Self::Low, + EffortRecord::Medium => Self::Medium, + EffortRecord::High => Self::High, + EffortRecord::XHigh => Self::XHigh, + EffortRecord::Max => Self::Max, } } } diff --git a/crates/forge_repo/src/provider/openai_responses/request.rs b/crates/forge_repo/src/provider/openai_responses/request.rs index 07546c5933..2ada99c894 100644 --- a/crates/forge_repo/src/provider/openai_responses/request.rs +++ b/crates/forge_repo/src/provider/openai_responses/request.rs @@ -113,9 +113,13 @@ impl FromDomain for oai::Reasoning { // Map effort level if let Some(effort) = config.effort { let oai_effort = match effort { - Effort::High => oai::ReasoningEffort::High, - Effort::Medium => oai::ReasoningEffort::Medium, + Effort::None => oai::ReasoningEffort::None, + Effort::Minimal => oai::ReasoningEffort::Minimal, Effort::Low => oai::ReasoningEffort::Low, + Effort::Medium => oai::ReasoningEffort::Medium, + Effort::High => oai::ReasoningEffort::High, + // XHigh and Max both map to the highest available OAI level. + Effort::XHigh | Effort::Max => oai::ReasoningEffort::Xhigh, }; builder.effort(oai_effort); } else if config.enabled.unwrap_or(false) { From ca6f3dd2503778554882d00c60475e34ef339acc Mon Sep 17 00:00:00 2001 From: Tushar Date: Thu, 2 Apr 2026 01:03:53 +0530 Subject: [PATCH 11/25] refactor(reasoning): expand Effort enum and update descriptions --- forge.schema.json | 53 ++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 43 insertions(+), 10 deletions(-) diff --git a/forge.schema.json b/forge.schema.json index 96d5563bd4..9ba1a41bba 100644 --- a/forge.schema.json +++ b/forge.schema.json @@ -207,7 +207,7 @@ "minimum": 0 }, "reasoning": { - "description": "Reasoning settings applied to all agents; individual agent settings\ntake priority over this global default via merge semantics.", + "description": "Reasoning configuration applied to all agents; controls effort level,\ntoken budget, and visibility of the model's thinking process.", "anyOf": [ { "$ref": "#/$defs/ReasoningConfig" @@ -407,11 +407,43 @@ } }, "Effort": { - "type": "string", - "enum": [ - "high", - "medium", - "low" + "description": "Effort level for model reasoning.", + "oneOf": [ + { + "description": "No reasoning; skips the thinking step entirely.", + "type": "string", + "const": "none" + }, + { + "description": "Minimal reasoning; fastest and cheapest.", + "type": "string", + "const": "minimal" + }, + { + "description": "Low reasoning effort.", + "type": "string", + "const": "low" + }, + { + "description": "Medium reasoning effort; the default for most providers.", + "type": "string", + "const": "medium" + }, + { + "description": "High reasoning effort.", + "type": "string", + "const": "high" + }, + { + "description": "Extra-high reasoning effort (OpenAI / OpenRouter).", + "type": "string", + "const": "xhigh" + }, + { + "description": "Maximum reasoning effort; only available on select Anthropic models.", + "type": "string", + "const": "max" + } ] }, "HttpConfig": { @@ -544,10 +576,11 @@ } }, "ReasoningConfig": { + "description": "Controls the reasoning behaviour of a model, including effort level, token\nbudget, and visibility of the thinking process.", "type": "object", "properties": { "effort": { - "description": "Controls the effort level of the agent's reasoning\nsupported by openrouter and forge provider", + "description": "Controls the effort level of the model's reasoning.\nSupported by openrouter and forge provider.", "anyOf": [ { "$ref": "#/$defs/Effort" @@ -558,21 +591,21 @@ ] }, "enabled": { - "description": "Enables reasoning at the \"medium\" effort level with no exclusions.\nsupported by openrouter, anthropic and forge provider", + "description": "Enables reasoning at the \"medium\" effort level with no exclusions.\nSupported by openrouter, anthropic, and forge provider.", "type": [ "boolean", "null" ] }, "exclude": { - "description": "Model thinks deeply, but the reasoning is hidden from you.\nsupported by openrouter and forge provider", + "description": "When true, the model thinks deeply but the reasoning is hidden from the\ncaller. Supported by openrouter and forge provider.", "type": [ "boolean", "null" ] }, "max_tokens": { - "description": "Controls how many tokens the model can spend thinking.\nsupported by openrouter, anthropic and forge provider\nshould be greater then 1024 but less than overall max_tokens", + "description": "Controls how many tokens the model can spend thinking.\nShould be greater than 1024 but less than the overall max_tokens.\nSupported by openrouter, anthropic, and forge provider.", "type": [ "integer", "null" From 905e2f78605f069b1fac87bc3a22e6ce4309b2b6 Mon Sep 17 00:00:00 2001 From: Tushar Date: Thu, 2 Apr 2026 11:19:03 +0530 Subject: [PATCH 12/25] feat(reasoning): add comprehensive reasoning serialization tests and scripts --- .forge/skills/test-reasoning/SKILL.md | 52 +++++ .../test-reasoning/scripts/test-reasoning.sh | 185 ++++++++++++++++++ docs/reasoning-test.md | 168 ---------------- 3 files changed, 237 insertions(+), 168 deletions(-) create mode 100644 .forge/skills/test-reasoning/SKILL.md create mode 100755 .forge/skills/test-reasoning/scripts/test-reasoning.sh delete mode 100644 docs/reasoning-test.md diff --git a/.forge/skills/test-reasoning/SKILL.md b/.forge/skills/test-reasoning/SKILL.md new file mode 100644 index 0000000000..0eedff054f --- /dev/null +++ b/.forge/skills/test-reasoning/SKILL.md @@ -0,0 +1,52 @@ +--- +name: test-reasoning +description: Validate that reasoning parameters are correctly serialized and sent to provider APIs. Use when the user asks to test reasoning serialization, run reasoning tests, verify reasoning config fields, or check that ReasoningConfig maps correctly to provider-specific JSON (OpenRouter, Anthropic, GitHub Copilot, Codex). +--- + +# Test Reasoning Serialization + +Validates that `ReasoningConfig` fields are correctly serialized into provider-specific JSON +for OpenRouter, Anthropic, GitHub Copilot, and Codex. + +## Quick Start + +Run all tests with the bundled script: + +```bash +./scripts/test-reasoning.sh +``` + +The script builds forge in debug mode, runs each provider/model combination, captures the +outgoing HTTP request body via `FORGE_DEBUG_REQUESTS`, and asserts the correct JSON fields. + +## Running a Single Test Manually + +```bash +FORGE_DEBUG_REQUESTS="forge.request.json" \ +FORGE_SESSION__PROVIDER_ID= \ +FORGE_SESSION__MODEL_ID= \ +FORGE_REASONING__EFFORT= \ +target/debug/forge -p "Hello!" +``` + +Then inspect `.forge/forge.request.json` for the expected fields. + +## Test Coverage + +| # | Provider | Model | Config fields | Expected JSON field | +| --- | ---------------- | ---------------------------- | ------------------------------------ | --------------------------------- | +| 1 | `open_router` | `openai/o4-mini` | `effort: high` | `reasoning.effort` | +| 2 | `open_router` | `anthropic/claude-opus-4-5` | `max_tokens: 4000` | `reasoning.max_tokens` | +| 3 | `anthropic` | `claude-opus-4-6` | `effort: medium` | `output_config.effort` | +| 4 | `anthropic` | `claude-3-7-sonnet-20250219` | `enabled: true` + `max_tokens: 8000` | `thinking.type` + `budget_tokens` | +| 5 | `github_copilot` | `o4-mini` | `effort: medium` | `reasoning_effort` (top-level) | +| 6 | `codex` | `gpt-5.1-codex` | `effort: medium` | `reasoning.effort` + `.summary` | + +Tests for unconfigured providers are skipped automatically. + +## References + +- [OpenAI Reasoning guide](https://developers.openai.com/api/docs/guides/reasoning) +- [OpenAI Chat Completions API reference](https://developers.openai.com/api/reference/resources/chat/subresources/completions/methods/create) +- [Anthropic Extended Thinking](https://platform.claude.com/docs/en/build-with-claude/effort) +- [OpenRouter Reasoning Tokens](https://openrouter.ai/docs/guides/best-practices/reasoning-tokens) diff --git a/.forge/skills/test-reasoning/scripts/test-reasoning.sh b/.forge/skills/test-reasoning/scripts/test-reasoning.sh new file mode 100755 index 0000000000..ddd9220794 --- /dev/null +++ b/.forge/skills/test-reasoning/scripts/test-reasoning.sh @@ -0,0 +1,185 @@ +#!/usr/bin/env bash +# scripts/test-reasoning.sh +# +# Validates that reasoning parameters are correctly serialized for each +# provider. Runs all entries in docs/reasoning-test.md plus Chat Completions +# (GitHub Copilot) and Responses API (Codex) paths. +# +# Usage: ./scripts/test-reasoning.sh + +set -uo pipefail + +# ─── colors ─────────────────────────────────────────────────────────────────── + +BOLD='\033[1m' +RESET='\033[0m' +GREEN='\033[32m' +RED='\033[31m' +YELLOW='\033[33m' +CYAN='\033[36m' +DIM='\033[2m' + +# ─── state ──────────────────────────────────────────────────────────────────── + +PASS=0 +FAIL=0 +SKIP=0 +BINARY="target/debug/forge" +WORK_DIR="$(mktemp -d)" + +cleanup() { rm -rf "$WORK_DIR"; } +trap cleanup EXIT + +# ─── output helpers ─────────────────────────────────────────────────────────── + +log_header() { printf "\n${BOLD}${CYAN}▶ %s${RESET}\n" "$1"; } +log_pass() { printf " ${GREEN}✓${RESET} %s\n" "$1"; PASS=$((PASS + 1)); } +log_fail() { printf " ${RED}✗${RESET} %s\n" "$1"; FAIL=$((FAIL + 1)); } +log_skip() { printf " ${YELLOW}~${RESET} %s\n" "$1"; SKIP=$((SKIP + 1)); } + +# ─── json helpers ───────────────────────────────────────────────────────────── + +# json_get +# Prints the JSON value at the given path, or "null" if absent/null. +json_get() { + python3 - "$1" "$2" <<'PY' +import json, sys +with open(sys.argv[1]) as f: + d = json.load(f) +keys = sys.argv[2].split('.') +v = d +for k in keys: + v = v.get(k) if isinstance(v, dict) else None + if v is None: + break +print(json.dumps(v)) +PY +} + +# assert_field