diff --git a/src/arbiterAI/arbiterAI.h b/src/arbiterAI/arbiterAI.h index 0f8d540..499ff2d 100644 --- a/src/arbiterAI/arbiterAI.h +++ b/src/arbiterAI/arbiterAI.h @@ -359,6 +359,7 @@ struct CompletionRequest std::optional> stop; std::optional> tools; ///< Available tools for the model std::optional tool_choice; ///< Tool selection mode: "auto", "none", or specific tool name + std::optional> logit_bias; ///< Token ID to bias value }; inline void to_json(nlohmann::json &j, const CompletionRequest &r) @@ -429,6 +430,7 @@ inline void from_json(const nlohmann::json &j, Usage &u) struct CompletionResponse { std::string text; + std::string reasoningContent; ///< Chain-of-thought / reasoning (e.g. DeepSeek reasoning_content) std::string model; Usage usage; std::string provider; // "openai", "anthropic", etc. @@ -449,6 +451,7 @@ inline void to_json(nlohmann::json &j, const CompletionResponse &r) {"finish_reason", r.finishReason}, {"from_cache", r.fromCache} }; + if (!r.reasoningContent.empty()) j["reasoning_content"] = r.reasoningContent; if (!r.toolCalls.empty()) j["tool_calls"] = r.toolCalls; } @@ -458,6 +461,7 @@ inline void from_json(const nlohmann::json &j, CompletionResponse &r) j.at("model").get_to(r.model); j.at("usage").get_to(r.usage); j.at("provider").get_to(r.provider); + if (j.contains("reasoning_content")) j.at("reasoning_content").get_to(r.reasoningContent); if (j.contains("cost")) j.at("cost").get_to(r.cost); if (j.contains("tool_calls")) j.at("tool_calls").get_to(r.toolCalls); if (j.contains("finish_reason")) j.at("finish_reason").get_to(r.finishReason); diff --git a/src/arbiterAI/providers/deepseek.cpp b/src/arbiterAI/providers/deepseek.cpp index 09c3387..ab670e7 100644 --- a/src/arbiterAI/providers/deepseek.cpp +++ b/src/arbiterAI/providers/deepseek.cpp @@ -112,13 +112,31 @@ ErrorCode Deepseek::parseResponse(const cpr::Response &rawResponse, // Extract the response text from the first choice if(!jsonResponse.contains("choices")|| jsonResponse["choices"].empty()|| - !jsonResponse["choices"][0].contains("message")|| - !jsonResponse["choices"][0]["message"].contains("content")) + !jsonResponse["choices"][0].contains("message")) { return ErrorCode::InvalidResponse; } - response.text=jsonResponse["choices"][0]["message"]["content"]; + const auto &message = jsonResponse["choices"][0]["message"]; + + // Extract reasoning_content (chain-of-thought) + if(message.contains("reasoning_content") && !message["reasoning_content"].is_null()) + { + response.reasoningContent = message["reasoning_content"].get(); + } + + // Extract content + if(message.contains("content") && !message["content"].is_null()) + { + response.text = message["content"].get(); + } + + // Fallback: use reasoning as text if content is empty + if(response.text.empty() && !response.reasoningContent.empty()) + { + response.text = response.reasoningContent; + } + response.provider="deepseek"; if(jsonResponse.contains("model")) diff --git a/src/arbiterAI/providers/openai.cpp b/src/arbiterAI/providers/openai.cpp index 43f34fb..298df42 100644 --- a/src/arbiterAI/providers/openai.cpp +++ b/src/arbiterAI/providers/openai.cpp @@ -263,6 +263,12 @@ ErrorCode OpenAI::parseResponse(const cpr::Response &rawResponse, response.finishReason = choice["finish_reason"].get(); } + // Extract reasoning_content (chain-of-thought from reasoning models) + if(message.contains("reasoning_content") && !message["reasoning_content"].is_null()) + { + response.reasoningContent = message["reasoning_content"].get(); + } + // Extract content (may be empty/null for tool_calls responses) if(message.contains("content") && !message["content"].is_null()) { @@ -327,6 +333,13 @@ ErrorCode OpenAI::parseResponse(const cpr::Response &rawResponse, response.usage.completion_tokens=jsonResponse["usage"]["completion_tokens"]; } + // If content is empty but we have reasoning, use reasoning as the text + // so callers that only read .text still get the model output + if(response.text.empty() && !response.reasoningContent.empty() && response.toolCalls.empty()) + { + response.text = response.reasoningContent; + } + return ErrorCode::Success; } @@ -366,12 +379,16 @@ ErrorCode OpenAI::streamingCompletion(const CompletionRequest &request, auto json=nlohmann::json::parse(jsonStr); if(json.contains("choices")&&!json["choices"].empty()&& - json["choices"][0].contains("delta")&& - json["choices"][0]["delta"].contains("content")) + json["choices"][0].contains("delta")) { - - std::string content=json["choices"][0]["delta"]["content"]; - callback(content); + const auto &delta = json["choices"][0]["delta"]; + std::string chunk; + if(delta.contains("reasoning_content") && !delta["reasoning_content"].is_null()) + chunk += delta["reasoning_content"].get(); + if(delta.contains("content") && !delta["content"].is_null()) + chunk += delta["content"].get(); + if(!chunk.empty()) + callback(chunk); } } } diff --git a/src/arbiterAI/providers/openrouter.cpp b/src/arbiterAI/providers/openrouter.cpp index cef3632..8ab9efe 100644 --- a/src/arbiterAI/providers/openrouter.cpp +++ b/src/arbiterAI/providers/openrouter.cpp @@ -66,7 +66,17 @@ ErrorCode OpenRouter_LLM::parseResponse(const cpr::Response &rawResponse, Comple { nlohmann::json jsonResponse=nlohmann::json::parse(rawResponse.text); const auto &choice=jsonResponse["choices"][0]; - response.text=choice["message"]["content"]; + const auto &message=choice["message"]; + + if(message.contains("reasoning_content") && !message["reasoning_content"].is_null()) + response.reasoningContent=message["reasoning_content"].get(); + + if(message.contains("content") && !message["content"].is_null()) + response.text=message["content"].get(); + + if(response.text.empty() && !response.reasoningContent.empty()) + response.text=response.reasoningContent; + response.model=jsonResponse["model"]; if(jsonResponse.contains("usage")) {