Files
hack/tools/scripts/llm-key-hunter/results/_cache/content/achieveai_lmdotnettools.json
T
OpenCode 68a9071cf1 feat: reconcile local master with origin (exa/hot-platform hunters, leaks ledger, latest results)
Local branch had diverged from origin/master (sibling commits on the same base). Rewrote local history linearly on top of origin/master, folding in all local content: exa/hot-platform discovery hunters, leaks ledger, nightly sweep orchestrator, updated .gitignore and skill docs, plus the latest hunt output and vault state. Remote-only files (vault keys, channel scripts) were restored rather than dropped, so the resulting tree is a full union of both sides.
2026-08-07 12:53:47 +08:00

1 line
503 KiB
JSON

{"repo": "achieveai/lmdotnettools", "files": {"example/LmConfigUsageExample/OpenAiGrokAgenticExample.cs": {"5df9a37e189f520689e30ebef085dd06c50d907c": {"t": 1785745808.2934623, "n": 17722, "h": "362d5ae78d9ce7a6", "c": "using System.Text.Json;\nusing AchieveAi.LmDotnetTools.LmConfig.Agents;\nusing AchieveAi.LmDotnetTools.LmCore.Core;\nusing AchieveAi.LmDotnetTools.LmCore.Messages;\nusing AchieveAi.LmDotnetTools.LmCore.Middleware;\nusing AchieveAi.LmDotnetTools.LmCore.Utils;\nusing Microsoft.Extensions.Logging;\n\nnamespace LmConfigUsageExample;\n\n/// <summary>\n/// Example demonstrating an agentic loop using OpenAI provider connected to Grok4.1\n/// with the new middleware chain architecture.\n///\n/// Middleware Chain:\n/// Provider Agent => MessageTransformationMiddleware => JsonFragmentUpdateMiddleware\n/// => MessageUpdateJoinerMiddleware => ToolCallInjectionMiddleware\n///\n/// Agentic Loop:\n/// 1. Call middleware chain (streaming)\n/// 2. Collect all messages from the stream\n/// 3. Find ToolsCallMessage in the messages\n/// 4. If found, use ToolCallExecutor to execute tool calls asynchronously\n/// 5. Add ToolsCallResultMessage to conversation\n/// 6. Loop again\n/// 7. If no ToolsCallMessage, end the loop\n/// </summary>\npublic class OpenAiGrokAgenticExample\n{\n private readonly IModelResolver _modelResolver;\n private readonly IProviderAgentFactory _agentFactory;\n private readonly ILogger<OpenAiGrokAgenticExample> _logger;\n private static readonly string[] stringArray = [\"sunny\", \"cloudy\", \"rainy\", \"partly cloudy\"];\n\n public OpenAiGrokAgenticExample(\n IModelResolver modelResolver,\n IProviderAgentFactory agentFactory,\n ILogger<OpenAiGrokAgenticExample> logger\n )\n {\n _modelResolver = modelResolver;\n _agentFactory = agentFactory;\n _logger = logger;\n }\n\n public async Task RunAsync(string prompt, string modelId = \"x-ai/grok-4.1-fast\", float temperature = 0.7f, int maxTurns = 10)\n {\n _logger.LogInformation(\"=== Agentic Loop Example with {ModelId} ===\\n\", modelId);\n\n // ===== Step 1: Resolve the specified model =====\n var resolution = await _modelResolver.ResolveProviderAsync(modelId);\n if (resolution == null)\n {\n _logger.LogError(\"Failed to resolve model: {ModelId}\", modelId);\n _logger.LogWarning(\"Make sure the model exists in models.json and the provider has an API key set.\");\n _logger.LogWarning(\"\");\n _logger.LogWarning(\"Required API keys by model:\");\n _logger.LogWarning(\" grok-4.1, x-ai/* -> XAI_API_KEY\");\n _logger.LogWarning(\" gpt-4.1*, openai/* -> OPENAI_API_KEY\");\n _logger.LogWarning(\" claude-3-*, anthropic/* -> ANTHROPIC_API_KEY\");\n _logger.LogWarning(\" openrouter/* -> OPENROUTER_API_KEY\");\n _logger.LogWarning(\" deepseek/* -> DEEPSEEK_API_KEY\");\n _logger.LogWarning(\" claude-sonnet-4-5 -> ClaudeAgentSDK (no API key needed if Claude Code authenticated)\");\n _logger.LogWarning(\"\");\n _logger.LogWarning(\"Use '--list-providers' to see provider status.\");\n return;\n }\n\n _logger.LogInformation(\"✓ Resolved model: {ModelName}\", resolution.EffectiveModelName);\n _logger.LogInformation(\" Provider: {ProviderName}\", resolution.EffectiveProviderName);\n _logger.LogInformation(\" Endpoint: {Endpoint}\\n\", resolution.Connection.EndpointUrl);\n\n // ===== Step 3: Define Tools using FunctionRegistry =====\n var registry = new FunctionRegistry();\n\n // get_weather\n _ = registry.AddFunction(\n new FunctionContract\n {\n Name = \"get_weather\",\n Description = \"Get the current weather for a location\",\n Parameters =\n [\n new FunctionParameterContract\n {\n Name = \"location\",\n Description = \"The city name (e.g., 'San Francisco', 'New York')\",\n ParameterType = SchemaHelper.CreateJsonSchemaFromType(typeof(string)),\n IsRequired = true,\n },\n new FunctionParameterContract\n {\n Name = \"unit\",\n Description = \"Temperature unit: 'celsius' or 'fahrenheit'\",\n ParameterType = SchemaHelper.CreateJsonSchemaFromType(typeof(string)),\n IsRequired = false,\n },\n ],\n },\n async (args, _, _) =>\n {\n var weatherArgs = JsonSerializer.Deserialize<WeatherArgs>(args);\n _logger.LogInformation(\"[TOOL] Getting weather for {Location}\", weatherArgs?.Location);\n\n await Task.Delay(100); // Simulate API call\n\n var weather = new\n {\n location = weatherArgs?.Location,\n temperature = Random.Shared.Next(60, 85),\n unit = weatherArgs?.Unit ?? \"fahrenheit\",\n condition = stringArray[Random.Shared.Next(4)],\n };\n\n return ToolHandlerResult.FromText(JsonSerializer.Serialize(weather));\n },\n providerName: \"Example\"\n );\n\n // get_time\n _ = registry.AddFunction(\n new FunctionContract\n {\n Name = \"get_time\",\n Description = \"Get the current time for a timezone\",\n Parameters =\n [\n new FunctionParameterContract\n {\n Name = \"timezone\",\n Description = \"Timezone (e.g., 'PST', 'EST', 'UTC')\",\n ParameterType = SchemaHelper.CreateJsonSchemaFromType(typeof(string)),\n IsRequired = true,\n },\n ],\n },\n async (args, _, _) =>\n {\n var timeArgs = JsonSerializer.Deserialize<TimeArgs>(args);\n _logger.LogInformation(\"[TOOL] Getting time for {Timezone}\", timeArgs?.Timezone);\n\n await Task.Delay(100); // Simulate API call\n\n var time = new\n {\n timezone = timeArgs?.Timezone,\n time = DateTime.UtcNow.ToString(\"HH:mm:ss\"),\n date = DateTime.UtcNow.ToString(\"yyyy-MM-dd\"),\n };\n\n return ToolHandlerResult.FromText(JsonSerializer.Serialize(time));\n },\n providerName: \"Example\"\n );\n\n // calculate\n _ = registry.AddFunction(\n new FunctionContract\n {\n Name = \"calculate\",\n Description = \"Perform a mathematical calculation\",\n Parameters =\n [\n new FunctionParameterContract\n {\n Name = \"expression\",\n Description = \"Mathematical expression (e.g., '2 + 2', '10 * 5')\",\n ParameterType = SchemaHelper.CreateJsonSchemaFromType(typeof(string)),\n IsRequired = true,\n },\n ],\n },\n async (args, _, _) =>\n {\n var calcArgs = JsonSerializer.Deserialize<CalculateArgs>(args);\n _logger.LogInformation(\"[TOOL] Calculating: {Expression}\", calcArgs?.Expression);\n\n await Task.Delay(50); // Simulate processing\n\n // Simple eval-like calculation (for demo purposes only - unsafe in production)\n try\n {\n var result = EvaluateSimpleExpression(calcArgs?.Expression ?? \"0\");\n return ToolHandlerResult.FromText(JsonSerializer.Serialize(new { result, expression = calcArgs?.Expression }));\n }\n catch (Exception ex)\n {\n return ToolHandlerResult.FromError(JsonSerializer.Serialize(new { error = ex.Message }));\n }\n },\n providerName: \"Example\"\n );\n\n // Build middleware and handlers from registry\n var (toolCallMiddleware, functionHandlers) = registry.BuildToolCallComponents(name: \"ToolCallInjection\");\n\n // ===== Step 4: Create Provider Agent =====\n var providerAgent = _agentFactory.CreateStreamingAgent(resolution)\n .WithMessageTransformation()\n .WithMiddleware(new JsonFragmentUpdateMiddleware())\n .WithMiddleware(new MessageUpdateJoinerMiddleware(name: \"MessageJoiner\"))\n .WithMiddleware(toolCallMiddleware);\n\n // ===== Step 6: Start Conversation =====\n var conversationHistory = new List<IMessage>\n {\n new TextMessage { Text = prompt, Role = Role.User },\n };\n\n _logger.LogInformation(\"User: {Prompt}\\n\", prompt);\n\n // ===== Step 7: Agentic Loop =====\n var turnCount = 0;\n\n while (turnCount < maxTurns)\n {\n turnCount++;\n _logger.LogInformation(\"=== Turn {Turn} ===\", turnCount);\n\n // Call LLM through middleware chain (streaming)\n var streamTask = await providerAgent.GenerateReplyStreamingAsync(\n conversationHistory,\n new GenerateReplyOptions\n {\n ModelId = modelId,\n // Temperature = temperature,\n RunId = Guid.NewGuid().ToString(),\n ParentRunId = null,\n ThreadId = Guid.NewGuid().ToString(),\n }\n );\n\n // Collect messages from stream and execute tool calls in parallel as they arrive\n var messages = new List<IMessage>();\n var textContent = new System.Text.StringBuilder();\n var pendingToolCalls = new Dictionary<string, Task<ToolCallResultMessage>>();\n\n await foreach (var message in streamTask)\n {\n messages.Add(message);\n\n // Display streaming content in real-time and handle tool calls\n switch (message)\n {\n case ReasoningMessage reasoningMsg:\n if (!string.IsNullOrEmpty(reasoningMsg.Reasoning))\n {\n Console.Write(reasoningMsg.Reasoning);\n _ = textContent.Append(reasoningMsg.Reasoning);\n }\n break;\n\n case TextMessage textMsg:\n if (!string.IsNullOrEmpty(textMsg.Text))\n {\n Console.Write(textMsg.Text);\n _ = textContent.Append(textMsg.Text);\n }\n break;\n\n case ToolCallMessage toolCall:\n // Start executing tool call immediately when we receive it\n _logger.LogInformation(\n \"[Tool Call] Starting execution: {FunctionName} (id: {ToolCallId})\",\n toolCall.FunctionName,\n toolCall.ToolCallId\n );\n\n var executionTask = ExecuteToolCallAsync(toolCall, functionHandlers);\n pendingToolCalls[toolCall.ToolCallId ?? $\"call_{pendingToolCalls.Count}\"] = executionTask;\n break;\n\n case UsageMessage usage:\n _logger.LogDebug(\n \"[Usage] Prompt: {Prompt}, Completion: {Completion}\",\n usage.Usage.PromptTokens,\n usage.Usage.CompletionTokens\n );\n break;\n\n default:\n // Ignore other message types\n break;\n }\n }\n\n Console.WriteLine(); // New line after streaming\n\n // Add all messages to conversation history\n conversationHistory.AddRange(messages);\n\n _logger.LogInformation(\"Received {Count} message(s)\", messages.Count);\n\n // ===== Step 8: Check for Tool Calls and Await Results =====\n if (pendingToolCalls.Count == 0)\n {\n // No tool calls, conversation complete\n _logger.LogInformation(\"✓ No tool calls - conversation complete\\n\");\n\n if (textContent.Length > 0)\n {\n _logger.LogInformation(\"Assistant: {Response}\", textContent.ToString());\n }\n\n break;\n }\n\n // ===== Step 9: Await Tool Results and Add to Conversation =====\n _logger.LogInformation(\"Awaiting {Count} tool call result(s)...\", pendingToolCalls.Count);\n\n try\n {\n // Await all pending tool executions\n _ = await Task.WhenAll(pendingToolCalls.Values);\n\n // Collect results and add to conversation\n foreach (var kvp in pendingToolCalls)\n {\n var result = await kvp.Value;\n var resultPreview = result.Result.Length > 100 ? result.Result[..100] + \"...\" : result.Result;\n _logger.LogInformation(\" Result for {ToolCallId}: {Result}\", kvp.Key, resultPreview);\n\n // Add each tool result to conversation history\n conversationHistory.Add(result);\n }\n\n _logger.LogInformation(\"Tool execution completed.\");\n Console.WriteLine(); // Spacing\n }\n catch (Exception ex)\n {\n _logger.LogError(ex, \"Tool execution failed\");\n\n // Add error message to conversation\n var errorMessage = new TextMessage { Text = $\"Error executing tools: {ex.Message}\", Role = Role.User };\n conversationHistory.Add(errorMessage);\n }\n }\n\n if (turnCount >= maxTurns)\n {\n _logger.LogWarning(\"⚠ Max turns reached - stopping conversation\");\n }\n\n _logger.LogInformation(\"\\n=== Agentic Loop Complete ===\");\n }\n\n // ===== Helper Methods =====\n\n /// <summary>\n /// Executes a single tool call and returns the result message.\n /// </summary>\n private async Task<ToolCallResultMessage> ExecuteToolCallAsync(\n ToolCallMessage toolCall,\n IDictionary<string, ToolHandler> handlers\n )\n {\n var toolCallId = toolCall.ToolCallId ?? $\"call_{toolCall.Index}\";\n var functionName = toolCall.FunctionName ?? \"unknown\";\n var functionArgs = toolCall.FunctionArgs ?? \"{}\";\n\n try\n {\n if (handlers.TryGetValue(functionName, out var handler))\n {\n var ctx = new ToolCallContext { ToolCallId = toolCall.ToolCallId };\n var result = await handler(functionArgs, ctx, CancellationToken.None);\n return new ToolCallResultMessage\n {\n ToolCallId = toolCallId,\n Result = result.ResultText,\n Role = Role.User,\n FromAgent = toolCall.FromAgent,\n GenerationId = toolCall.GenerationId,\n };\n }\n else\n {\n _logger.LogWarning(\"No handler found for function: {FunctionName}\", functionName);\n return new ToolCallResultMessage\n {\n ToolCallId = toolCallId,\n Result = JsonSerializer.Serialize(new { error = $\"Unknown function: {functionName}\" }),\n Role = Role.User,\n FromAgent = toolCall.FromAgent,\n GenerationId = toolCall.GenerationId,\n };\n }\n }\n catch (Exception ex)\n {\n _logger.LogError(ex, \"Error executing tool call: {FunctionName}\", functionName);\n return new ToolCallResultMessage\n {\n ToolCallId = toolCallId,\n Result = JsonSerializer.Serialize(new { error = ex.Message }),\n Role = Role.User,\n FromAgent = toolCall.FromAgent,\n GenerationId = toolCall.GenerationId,\n };\n }\n }\n\n // ===== Helper Classes =====\n\n private class WeatherArgs\n {\n public string? Location { get; set; }\n public string? Unit { get; set; }\n }\n\n private class TimeArgs\n {\n public string? Timezone { get; set; }\n }\n\n private class CalculateArgs\n {\n public string? Expression { get; set; }\n }\n\n // Simple expression evaluator (for demo purposes only - unsafe in production)\n private static double EvaluateSimpleExpression(string expression)\n {\n expression = expression.Replace(\" \", \"\");\n\n // Very basic calculator - only handles +, -, *, /\n if (expression.Contains('+'))\n {\n var parts = expression.Split('+');\n return double.Parse(parts[0]) + double.Parse(parts[1]);\n }\n if (expression.Contains('-'))\n {\n var parts = expression.Split('-');\n return double.Parse(parts[0]) - double.Parse(parts[1]);\n }\n if (expression.Contains('*'))\n {\n var parts = expression.Split('*');\n return double.Parse(parts[0]) * double.Parse(parts[1]);\n }\n if (expression.Contains('/'))\n {\n var parts = expression.Split('/');\n return double.Parse(parts[0]) / double.Parse(parts[1]);\n }\n\n return double.Parse(expression);\n }\n}\n"}, "26335e3cc6208c4d2aec959abf40b43d4a60047c": {"t": 1785756728.6472123, "n": 17722, "h": "362d5ae78d9ce7a6", "c": "using System.Text.Json;\nusing AchieveAi.LmDotnetTools.LmConfig.Agents;\nusing AchieveAi.LmDotnetTools.LmCore.Core;\nusing AchieveAi.LmDotnetTools.LmCore.Messages;\nusing AchieveAi.LmDotnetTools.LmCore.Middleware;\nusing AchieveAi.LmDotnetTools.LmCore.Utils;\nusing Microsoft.Extensions.Logging;\n\nnamespace LmConfigUsageExample;\n\n/// <summary>\n/// Example demonstrating an agentic loop using OpenAI provider connected to Grok4.1\n/// with the new middleware chain architecture.\n///\n/// Middleware Chain:\n/// Provider Agent => MessageTransformationMiddleware => JsonFragmentUpdateMiddleware\n/// => MessageUpdateJoinerMiddleware => ToolCallInjectionMiddleware\n///\n/// Agentic Loop:\n/// 1. Call middleware chain (streaming)\n/// 2. Collect all messages from the stream\n/// 3. Find ToolsCallMessage in the messages\n/// 4. If found, use ToolCallExecutor to execute tool calls asynchronously\n/// 5. Add ToolsCallResultMessage to conversation\n/// 6. Loop again\n/// 7. If no ToolsCallMessage, end the loop\n/// </summary>\npublic class OpenAiGrokAgenticExample\n{\n private readonly IModelResolver _modelResolver;\n private readonly IProviderAgentFactory _agentFactory;\n private readonly ILogger<OpenAiGrokAgenticExample> _logger;\n private static readonly string[] stringArray = [\"sunny\", \"cloudy\", \"rainy\", \"partly cloudy\"];\n\n public OpenAiGrokAgenticExample(\n IModelResolver modelResolver,\n IProviderAgentFactory agentFactory,\n ILogger<OpenAiGrokAgenticExample> logger\n )\n {\n _modelResolver = modelResolver;\n _agentFactory = agentFactory;\n _logger = logger;\n }\n\n public async Task RunAsync(string prompt, string modelId = \"x-ai/grok-4.1-fast\", float temperature = 0.7f, int maxTurns = 10)\n {\n _logger.LogInformation(\"=== Agentic Loop Example with {ModelId} ===\\n\", modelId);\n\n // ===== Step 1: Resolve the specified model =====\n var resolution = await _modelResolver.ResolveProviderAsync(modelId);\n if (resolution == null)\n {\n _logger.LogError(\"Failed to resolve model: {ModelId}\", modelId);\n _logger.LogWarning(\"Make sure the model exists in models.json and the provider has an API key set.\");\n _logger.LogWarning(\"\");\n _logger.LogWarning(\"Required API keys by model:\");\n _logger.LogWarning(\" grok-4.1, x-ai/* -> XAI_API_KEY\");\n _logger.LogWarning(\" gpt-4.1*, openai/* -> OPENAI_API_KEY\");\n _logger.LogWarning(\" claude-3-*, anthropic/* -> ANTHROPIC_API_KEY\");\n _logger.LogWarning(\" openrouter/* -> OPENROUTER_API_KEY\");\n _logger.LogWarning(\" deepseek/* -> DEEPSEEK_API_KEY\");\n _logger.LogWarning(\" claude-sonnet-4-5 -> ClaudeAgentSDK (no API key needed if Claude Code authenticated)\");\n _logger.LogWarning(\"\");\n _logger.LogWarning(\"Use '--list-providers' to see provider status.\");\n return;\n }\n\n _logger.LogInformation(\"✓ Resolved model: {ModelName}\", resolution.EffectiveModelName);\n _logger.LogInformation(\" Provider: {ProviderName}\", resolution.EffectiveProviderName);\n _logger.LogInformation(\" Endpoint: {Endpoint}\\n\", resolution.Connection.EndpointUrl);\n\n // ===== Step 3: Define Tools using FunctionRegistry =====\n var registry = new FunctionRegistry();\n\n // get_weather\n _ = registry.AddFunction(\n new FunctionContract\n {\n Name = \"get_weather\",\n Description = \"Get the current weather for a location\",\n Parameters =\n [\n new FunctionParameterContract\n {\n Name = \"location\",\n Description = \"The city name (e.g., 'San Francisco', 'New York')\",\n ParameterType = SchemaHelper.CreateJsonSchemaFromType(typeof(string)),\n IsRequired = true,\n },\n new FunctionParameterContract\n {\n Name = \"unit\",\n Description = \"Temperature unit: 'celsius' or 'fahrenheit'\",\n ParameterType = SchemaHelper.CreateJsonSchemaFromType(typeof(string)),\n IsRequired = false,\n },\n ],\n },\n async (args, _, _) =>\n {\n var weatherArgs = JsonSerializer.Deserialize<WeatherArgs>(args);\n _logger.LogInformation(\"[TOOL] Getting weather for {Location}\", weatherArgs?.Location);\n\n await Task.Delay(100); // Simulate API call\n\n var weather = new\n {\n location = weatherArgs?.Location,\n temperature = Random.Shared.Next(60, 85),\n unit = weatherArgs?.Unit ?? \"fahrenheit\",\n condition = stringArray[Random.Shared.Next(4)],\n };\n\n return ToolHandlerResult.FromText(JsonSerializer.Serialize(weather));\n },\n providerName: \"Example\"\n );\n\n // get_time\n _ = registry.AddFunction(\n new FunctionContract\n {\n Name = \"get_time\",\n Description = \"Get the current time for a timezone\",\n Parameters =\n [\n new FunctionParameterContract\n {\n Name = \"timezone\",\n Description = \"Timezone (e.g., 'PST', 'EST', 'UTC')\",\n ParameterType = SchemaHelper.CreateJsonSchemaFromType(typeof(string)),\n IsRequired = true,\n },\n ],\n },\n async (args, _, _) =>\n {\n var timeArgs = JsonSerializer.Deserialize<TimeArgs>(args);\n _logger.LogInformation(\"[TOOL] Getting time for {Timezone}\", timeArgs?.Timezone);\n\n await Task.Delay(100); // Simulate API call\n\n var time = new\n {\n timezone = timeArgs?.Timezone,\n time = DateTime.UtcNow.ToString(\"HH:mm:ss\"),\n date = DateTime.UtcNow.ToString(\"yyyy-MM-dd\"),\n };\n\n return ToolHandlerResult.FromText(JsonSerializer.Serialize(time));\n },\n providerName: \"Example\"\n );\n\n // calculate\n _ = registry.AddFunction(\n new FunctionContract\n {\n Name = \"calculate\",\n Description = \"Perform a mathematical calculation\",\n Parameters =\n [\n new FunctionParameterContract\n {\n Name = \"expression\",\n Description = \"Mathematical expression (e.g., '2 + 2', '10 * 5')\",\n ParameterType = SchemaHelper.CreateJsonSchemaFromType(typeof(string)),\n IsRequired = true,\n },\n ],\n },\n async (args, _, _) =>\n {\n var calcArgs = JsonSerializer.Deserialize<CalculateArgs>(args);\n _logger.LogInformation(\"[TOOL] Calculating: {Expression}\", calcArgs?.Expression);\n\n await Task.Delay(50); // Simulate processing\n\n // Simple eval-like calculation (for demo purposes only - unsafe in production)\n try\n {\n var result = EvaluateSimpleExpression(calcArgs?.Expression ?? \"0\");\n return ToolHandlerResult.FromText(JsonSerializer.Serialize(new { result, expression = calcArgs?.Expression }));\n }\n catch (Exception ex)\n {\n return ToolHandlerResult.FromError(JsonSerializer.Serialize(new { error = ex.Message }));\n }\n },\n providerName: \"Example\"\n );\n\n // Build middleware and handlers from registry\n var (toolCallMiddleware, functionHandlers) = registry.BuildToolCallComponents(name: \"ToolCallInjection\");\n\n // ===== Step 4: Create Provider Agent =====\n var providerAgent = _agentFactory.CreateStreamingAgent(resolution)\n .WithMessageTransformation()\n .WithMiddleware(new JsonFragmentUpdateMiddleware())\n .WithMiddleware(new MessageUpdateJoinerMiddleware(name: \"MessageJoiner\"))\n .WithMiddleware(toolCallMiddleware);\n\n // ===== Step 6: Start Conversation =====\n var conversationHistory = new List<IMessage>\n {\n new TextMessage { Text = prompt, Role = Role.User },\n };\n\n _logger.LogInformation(\"User: {Prompt}\\n\", prompt);\n\n // ===== Step 7: Agentic Loop =====\n var turnCount = 0;\n\n while (turnCount < maxTurns)\n {\n turnCount++;\n _logger.LogInformation(\"=== Turn {Turn} ===\", turnCount);\n\n // Call LLM through middleware chain (streaming)\n var streamTask = await providerAgent.GenerateReplyStreamingAsync(\n conversationHistory,\n new GenerateReplyOptions\n {\n ModelId = modelId,\n // Temperature = temperature,\n RunId = Guid.NewGuid().ToString(),\n ParentRunId = null,\n ThreadId = Guid.NewGuid().ToString(),\n }\n );\n\n // Collect messages from stream and execute tool calls in parallel as they arrive\n var messages = new List<IMessage>();\n var textContent = new System.Text.StringBuilder();\n var pendingToolCalls = new Dictionary<string, Task<ToolCallResultMessage>>();\n\n await foreach (var message in streamTask)\n {\n messages.Add(message);\n\n // Display streaming content in real-time and handle tool calls\n switch (message)\n {\n case ReasoningMessage reasoningMsg:\n if (!string.IsNullOrEmpty(reasoningMsg.Reasoning))\n {\n Console.Write(reasoningMsg.Reasoning);\n _ = textContent.Append(reasoningMsg.Reasoning);\n }\n break;\n\n case TextMessage textMsg:\n if (!string.IsNullOrEmpty(textMsg.Text))\n {\n Console.Write(textMsg.Text);\n _ = textContent.Append(textMsg.Text);\n }\n break;\n\n case ToolCallMessage toolCall:\n // Start executing tool call immediately when we receive it\n _logger.LogInformation(\n \"[Tool Call] Starting execution: {FunctionName} (id: {ToolCallId})\",\n toolCall.FunctionName,\n toolCall.ToolCallId\n );\n\n var executionTask = ExecuteToolCallAsync(toolCall, functionHandlers);\n pendingToolCalls[toolCall.ToolCallId ?? $\"call_{pendingToolCalls.Count}\"] = executionTask;\n break;\n\n case UsageMessage usage:\n _logger.LogDebug(\n \"[Usage] Prompt: {Prompt}, Completion: {Completion}\",\n usage.Usage.PromptTokens,\n usage.Usage.CompletionTokens\n );\n break;\n\n default:\n // Ignore other message types\n break;\n }\n }\n\n Console.WriteLine(); // New line after streaming\n\n // Add all messages to conversation history\n conversationHistory.AddRange(messages);\n\n _logger.LogInformation(\"Received {Count} message(s)\", messages.Count);\n\n // ===== Step 8: Check for Tool Calls and Await Results =====\n if (pendingToolCalls.Count == 0)\n {\n // No tool calls, conversation complete\n _logger.LogInformation(\"✓ No tool calls - conversation complete\\n\");\n\n if (textContent.Length > 0)\n {\n _logger.LogInformation(\"Assistant: {Response}\", textContent.ToString());\n }\n\n break;\n }\n\n // ===== Step 9: Await Tool Results and Add to Conversation =====\n _logger.LogInformation(\"Awaiting {Count} tool call result(s)...\", pendingToolCalls.Count);\n\n try\n {\n // Await all pending tool executions\n _ = await Task.WhenAll(pendingToolCalls.Values);\n\n // Collect results and add to conversation\n foreach (var kvp in pendingToolCalls)\n {\n var result = await kvp.Value;\n var resultPreview = result.Result.Length > 100 ? result.Result[..100] + \"...\" : result.Result;\n _logger.LogInformation(\" Result for {ToolCallId}: {Result}\", kvp.Key, resultPreview);\n\n // Add each tool result to conversation history\n conversationHistory.Add(result);\n }\n\n _logger.LogInformation(\"Tool execution completed.\");\n Console.WriteLine(); // Spacing\n }\n catch (Exception ex)\n {\n _logger.LogError(ex, \"Tool execution failed\");\n\n // Add error message to conversation\n var errorMessage = new TextMessage { Text = $\"Error executing tools: {ex.Message}\", Role = Role.User };\n conversationHistory.Add(errorMessage);\n }\n }\n\n if (turnCount >= maxTurns)\n {\n _logger.LogWarning(\"⚠ Max turns reached - stopping conversation\");\n }\n\n _logger.LogInformation(\"\\n=== Agentic Loop Complete ===\");\n }\n\n // ===== Helper Methods =====\n\n /// <summary>\n /// Executes a single tool call and returns the result message.\n /// </summary>\n private async Task<ToolCallResultMessage> ExecuteToolCallAsync(\n ToolCallMessage toolCall,\n IDictionary<string, ToolHandler> handlers\n )\n {\n var toolCallId = toolCall.ToolCallId ?? $\"call_{toolCall.Index}\";\n var functionName = toolCall.FunctionName ?? \"unknown\";\n var functionArgs = toolCall.FunctionArgs ?? \"{}\";\n\n try\n {\n if (handlers.TryGetValue(functionName, out var handler))\n {\n var ctx = new ToolCallContext { ToolCallId = toolCall.ToolCallId };\n var result = await handler(functionArgs, ctx, CancellationToken.None);\n return new ToolCallResultMessage\n {\n ToolCallId = toolCallId,\n Result = result.ResultText,\n Role = Role.User,\n FromAgent = toolCall.FromAgent,\n GenerationId = toolCall.GenerationId,\n };\n }\n else\n {\n _logger.LogWarning(\"No handler found for function: {FunctionName}\", functionName);\n return new ToolCallResultMessage\n {\n ToolCallId = toolCallId,\n Result = JsonSerializer.Serialize(new { error = $\"Unknown function: {functionName}\" }),\n Role = Role.User,\n FromAgent = toolCall.FromAgent,\n GenerationId = toolCall.GenerationId,\n };\n }\n }\n catch (Exception ex)\n {\n _logger.LogError(ex, \"Error executing tool call: {FunctionName}\", functionName);\n return new ToolCallResultMessage\n {\n ToolCallId = toolCallId,\n Result = JsonSerializer.Serialize(new { error = ex.Message }),\n Role = Role.User,\n FromAgent = toolCall.FromAgent,\n GenerationId = toolCall.GenerationId,\n };\n }\n }\n\n // ===== Helper Classes =====\n\n private class WeatherArgs\n {\n public string? Location { get; set; }\n public string? Unit { get; set; }\n }\n\n private class TimeArgs\n {\n public string? Timezone { get; set; }\n }\n\n private class CalculateArgs\n {\n public string? Expression { get; set; }\n }\n\n // Simple expression evaluator (for demo purposes only - unsafe in production)\n private static double EvaluateSimpleExpression(string expression)\n {\n expression = expression.Replace(\" \", \"\");\n\n // Very basic calculator - only handles +, -, *, /\n if (expression.Contains('+'))\n {\n var parts = expression.Split('+');\n return double.Parse(parts[0]) + double.Parse(parts[1]);\n }\n if (expression.Contains('-'))\n {\n var parts = expression.Split('-');\n return double.Parse(parts[0]) - double.Parse(parts[1]);\n }\n if (expression.Contains('*'))\n {\n var parts = expression.Split('*');\n return double.Parse(parts[0]) * double.Parse(parts[1]);\n }\n if (expression.Contains('/'))\n {\n var parts = expression.Split('/');\n return double.Parse(parts[0]) / double.Parse(parts[1]);\n }\n\n return double.Parse(expression);\n }\n}\n"}, "56588f18cc14eb718666114e86024ad32c2b7a6d": {"t": 1785756730.2002761, "n": 18173, "h": "77c43f364c259cca", "c": "using System.Text.Json;\r\nusing AchieveAi.LmDotnetTools.LmConfig.Agents;\r\nusing AchieveAi.LmDotnetTools.LmCore.Core;\r\nusing AchieveAi.LmDotnetTools.LmCore.Messages;\r\nusing AchieveAi.LmDotnetTools.LmCore.Middleware;\r\nusing AchieveAi.LmDotnetTools.LmCore.Utils;\r\nusing Microsoft.Extensions.Logging;\r\n\r\nnamespace LmConfigUsageExample;\r\n\r\n/// <summary>\r\n/// Example demonstrating an agentic loop using OpenAI provider connected to Grok4.1\r\n/// with the new middleware chain architecture.\r\n///\r\n/// Middleware Chain:\r\n/// Provider Agent => MessageTransformationMiddleware => JsonFragmentUpdateMiddleware\r\n/// => MessageUpdateJoinerMiddleware => ToolCallInjectionMiddleware\r\n///\r\n/// Agentic Loop:\r\n/// 1. Call middleware chain (streaming)\r\n/// 2. Collect all messages from the stream\r\n/// 3. Find ToolsCallMessage in the messages\r\n/// 4. If found, use ToolCallExecutor to execute tool calls asynchronously\r\n/// 5. Add ToolsCallResultMessage to conversation\r\n/// 6. Loop again\r\n/// 7. If no ToolsCallMessage, end the loop\r\n/// </summary>\r\npublic class OpenAiGrokAgenticExample\r\n{\r\n private readonly IModelResolver _modelResolver;\r\n private readonly IProviderAgentFactory _agentFactory;\r\n private readonly ILogger<OpenAiGrokAgenticExample> _logger;\r\n private static readonly string[] stringArray = [\"sunny\", \"cloudy\", \"rainy\", \"partly cloudy\"];\r\n\r\n public OpenAiGrokAgenticExample(\r\n IModelResolver modelResolver,\r\n IProviderAgentFactory agentFactory,\r\n ILogger<OpenAiGrokAgenticExample> logger\r\n )\r\n {\r\n _modelResolver = modelResolver;\r\n _agentFactory = agentFactory;\r\n _logger = logger;\r\n }\r\n\r\n public async Task RunAsync(string prompt, string modelId = \"x-ai/grok-4.1-fast\", float temperature = 0.7f, int maxTurns = 10)\r\n {\r\n _logger.LogInformation(\"=== Agentic Loop Example with {ModelId} ===\\n\", modelId);\r\n\r\n // ===== Step 1: Resolve the specified model =====\r\n var resolution = await _modelResolver.ResolveProviderAsync(modelId);\r\n if (resolution == null)\r\n {\r\n _logger.LogError(\"Failed to resolve model: {ModelId}\", modelId);\r\n _logger.LogWarning(\"Make sure the model exists in models.json and the provider has an API key set.\");\r\n _logger.LogWarning(\"\");\r\n _logger.LogWarning(\"Required API keys by model:\");\r\n _logger.LogWarning(\" grok-4.1, x-ai/* -> XAI_API_KEY\");\r\n _logger.LogWarning(\" gpt-4.1*, openai/* -> OPENAI_API_KEY\");\r\n _logger.LogWarning(\" claude-3-*, anthropic/* -> ANTHROPIC_API_KEY\");\r\n _logger.LogWarning(\" openrouter/* -> OPENROUTER_API_KEY\");\r\n _logger.LogWarning(\" deepseek/* -> DEEPSEEK_API_KEY\");\r\n _logger.LogWarning(\" claude-sonnet-4-5 -> ClaudeAgentSDK (no API key needed if Claude Code authenticated)\");\r\n _logger.LogWarning(\"\");\r\n _logger.LogWarning(\"Use '--list-providers' to see provider status.\");\r\n return;\r\n }\r\n\r\n _logger.LogInformation(\"✓ Resolved model: {ModelName}\", resolution.EffectiveModelName);\r\n _logger.LogInformation(\" Provider: {ProviderName}\", resolution.EffectiveProviderName);\r\n _logger.LogInformation(\" Endpoint: {Endpoint}\\n\", resolution.Connection.EndpointUrl);\r\n\r\n // ===== Step 3: Define Tools using FunctionRegistry =====\r\n var registry = new FunctionRegistry();\r\n\r\n // get_weather\r\n _ = registry.AddFunction(\r\n new FunctionContract\r\n {\r\n Name = \"get_weather\",\r\n Description = \"Get the current weather for a location\",\r\n Parameters =\r\n [\r\n new FunctionParameterContract\r\n {\r\n Name = \"location\",\r\n Description = \"The city name (e.g., 'San Francisco', 'New York')\",\r\n ParameterType = SchemaHelper.CreateJsonSchemaFromType(typeof(string)),\r\n IsRequired = true,\r\n },\r\n new FunctionParameterContract\r\n {\r\n Name = \"unit\",\r\n Description = \"Temperature unit: 'celsius' or 'fahrenheit'\",\r\n ParameterType = SchemaHelper.CreateJsonSchemaFromType(typeof(string)),\r\n IsRequired = false,\r\n },\r\n ],\r\n },\r\n async (args, _, _) =>\r\n {\r\n var weatherArgs = JsonSerializer.Deserialize<WeatherArgs>(args);\r\n _logger.LogInformation(\"[TOOL] Getting weather for {Location}\", weatherArgs?.Location);\r\n\r\n await Task.Delay(100); // Simulate API call\r\n\r\n var weather = new\r\n {\r\n location = weatherArgs?.Location,\r\n temperature = Random.Shared.Next(60, 85),\r\n unit = weatherArgs?.Unit ?? \"fahrenheit\",\r\n condition = stringArray[Random.Shared.Next(4)],\r\n };\r\n\r\n return ToolHandlerResult.FromText(JsonSerializer.Serialize(weather));\r\n },\r\n providerName: \"Example\"\r\n );\r\n\r\n // get_time\r\n _ = registry.AddFunction(\r\n new FunctionContract\r\n {\r\n Name = \"get_time\",\r\n Description = \"Get the current time for a timezone\",\r\n Parameters =\r\n [\r\n new FunctionParameterContract\r\n {\r\n Name = \"timezone\",\r\n Description = \"Timezone (e.g., 'PST', 'EST', 'UTC')\",\r\n ParameterType = SchemaHelper.CreateJsonSchemaFromType(typeof(string)),\r\n IsRequired = true,\r\n },\r\n ],\r\n },\r\n async (args, _, _) =>\r\n {\r\n var timeArgs = JsonSerializer.Deserialize<TimeArgs>(args);\r\n _logger.LogInformation(\"[TOOL] Getting time for {Timezone}\", timeArgs?.Timezone);\r\n\r\n await Task.Delay(100); // Simulate API call\r\n\r\n var time = new\r\n {\r\n timezone = timeArgs?.Timezone,\r\n time = DateTime.UtcNow.ToString(\"HH:mm:ss\"),\r\n date = DateTime.UtcNow.ToString(\"yyyy-MM-dd\"),\r\n };\r\n\r\n return ToolHandlerResult.FromText(JsonSerializer.Serialize(time));\r\n },\r\n providerName: \"Example\"\r\n );\r\n\r\n // calculate\r\n _ = registry.AddFunction(\r\n new FunctionContract\r\n {\r\n Name = \"calculate\",\r\n Description = \"Perform a mathematical calculation\",\r\n Parameters =\r\n [\r\n new FunctionParameterContract\r\n {\r\n Name = \"expression\",\r\n Description = \"Mathematical expression (e.g., '2 + 2', '10 * 5')\",\r\n ParameterType = SchemaHelper.CreateJsonSchemaFromType(typeof(string)),\r\n IsRequired = true,\r\n },\r\n ],\r\n },\r\n async (args, _, _) =>\r\n {\r\n var calcArgs = JsonSerializer.Deserialize<CalculateArgs>(args);\r\n _logger.LogInformation(\"[TOOL] Calculating: {Expression}\", calcArgs?.Expression);\r\n\r\n await Task.Delay(50); // Simulate processing\r\n\r\n // Simple eval-like calculation (for demo purposes only - unsafe in production)\r\n try\r\n {\r\n var result = EvaluateSimpleExpression(calcArgs?.Expression ?? \"0\");\r\n return ToolHandlerResult.FromText(JsonSerializer.Serialize(new { result, expression = calcArgs?.Expression }));\r\n }\r\n catch (Exception ex)\r\n {\r\n return ToolHandlerResult.FromError(JsonSerializer.Serialize(new { error = ex.Message }));\r\n }\r\n },\r\n providerName: \"Example\"\r\n );\r\n\r\n // Build middleware and handlers from registry\r\n var (toolCallMiddleware, functionHandlers) = registry.BuildToolCallComponents(name: \"ToolCallInjection\");\r\n\r\n // ===== Step 4: Create Provider Agent =====\r\n var providerAgent = _agentFactory.CreateStreamingAgent(resolution)\r\n .WithMessageTransformation()\r\n .WithMiddleware(new JsonFragmentUpdateMiddleware())\r\n .WithMiddleware(new MessageUpdateJoinerMiddleware(name: \"MessageJoiner\"))\r\n .WithMiddleware(toolCallMiddleware);\r\n\r\n // ===== Step 6: Start Conversation =====\r\n var conversationHistory = new List<IMessage>\r\n {\r\n new TextMessage { Text = prompt, Role = Role.User },\r\n };\r\n\r\n _logger.LogInformation(\"User: {Prompt}\\n\", prompt);\r\n\r\n // ===== Step 7: Agentic Loop =====\r\n var turnCount = 0;\r\n\r\n while (turnCount < maxTurns)\r\n {\r\n turnCount++;\r\n _logger.LogInformation(\"=== Turn {Turn} ===\", turnCount);\r\n\r\n // Call LLM through middleware chain (streaming)\r\n var streamTask = await providerAgent.GenerateReplyStreamingAsync(\r\n conversationHistory,\r\n new GenerateReplyOptions\r\n {\r\n ModelId = modelId,\r\n // Temperature = temperature,\r\n RunId = Guid.NewGuid().ToString(),\r\n ParentRunId = null,\r\n ThreadId = Guid.NewGuid().ToString(),\r\n }\r\n );\r\n\r\n // Collect messages from stream and execute tool calls in parallel as they arrive\r\n var messages = new List<IMessage>();\r\n var textContent = new System.Text.StringBuilder();\r\n var pendingToolCalls = new Dictionary<string, Task<ToolCallResultMessage>>();\r\n\r\n await foreach (var message in streamTask)\r\n {\r\n messages.Add(message);\r\n\r\n // Display streaming content in real-time and handle tool calls\r\n switch (message)\r\n {\r\n case ReasoningMessage reasoningMsg:\r\n if (!string.IsNullOrEmpty(reasoningMsg.Reasoning))\r\n {\r\n Console.Write(reasoningMsg.Reasoning);\r\n _ = textContent.Append(reasoningMsg.Reasoning);\r\n }\r\n break;\r\n\r\n case TextMessage textMsg:\r\n if (!string.IsNullOrEmpty(textMsg.Text))\r\n {\r\n Console.Write(textMsg.Text);\r\n _ = textContent.Append(textMsg.Text);\r\n }\r\n break;\r\n\r\n case ToolCallMessage toolCall:\r\n // Start executing tool call immediately when we receive it\r\n _logger.LogInformation(\r\n \"[Tool Call] Starting execution: {FunctionName} (id: {ToolCallId})\",\r\n toolCall.FunctionName,\r\n toolCall.ToolCallId\r\n );\r\n\r\n var executionTask = ExecuteToolCallAsync(toolCall, functionHandlers);\r\n pendingToolCalls[toolCall.ToolCallId ?? $\"call_{pendingToolCalls.Count}\"] = executionTask;\r\n break;\r\n\r\n case UsageMessage usage:\r\n _logger.LogDebug(\r\n \"[Usage] Prompt: {Prompt}, Completion: {Completion}\",\r\n usage.Usage.PromptTokens,\r\n usage.Usage.CompletionTokens\r\n );\r\n break;\r\n\r\n default:\r\n // Ignore other message types\r\n break;\r\n }\r\n }\r\n\r\n Console.WriteLine(); // New line after streaming\r\n\r\n // Add all messages to conversation history\r\n conversationHistory.AddRange(messages);\r\n\r\n _logger.LogInformation(\"Received {Count} message(s)\", messages.Count);\r\n\r\n // ===== Step 8: Check for Tool Calls and Await Results =====\r\n if (pendingToolCalls.Count == 0)\r\n {\r\n // No tool calls, conversation complete\r\n _logger.LogInformation(\"✓ No tool calls - conversation complete\\n\");\r\n\r\n if (textContent.Length > 0)\r\n {\r\n _logger.LogInformation(\"Assistant: {Response}\", textContent.ToString());\r\n }\r\n\r\n break;\r\n }\r\n\r\n // ===== Step 9: Await Tool Results and Add to Conversation =====\r\n _logger.LogInformation(\"Awaiting {Count} tool call result(s)...\", pendingToolCalls.Count);\r\n\r\n try\r\n {\r\n // Await all pending tool executions\r\n _ = await Task.WhenAll(pendingToolCalls.Values);\r\n\r\n // Collect results and add to conversation\r\n foreach (var kvp in pendingToolCalls)\r\n {\r\n var result = await kvp.Value;\r\n var resultPreview = result.Result.Length > 100 ? result.Result[..100] + \"...\" : result.Result;\r\n _logger.LogInformation(\" Result for {ToolCallId}: {Result}\", kvp.Key, resultPreview);\r\n\r\n // Add each tool result to conversation history\r\n conversationHistory.Add(result);\r\n }\r\n\r\n _logger.LogInformation(\"Tool execution completed.\");\r\n Console.WriteLine(); // Spacing\r\n }\r\n catch (Exception ex)\r\n {\r\n _logger.LogError(ex, \"Tool execution failed\");\r\n\r\n // Add error message to conversation\r\n var errorMessage = new TextMessage { Text = $\"Error executing tools: {ex.Message}\", Role = Role.User };\r\n conversationHistory.Add(errorMessage);\r\n }\r\n }\r\n\r\n if (turnCount >= maxTurns)\r\n {\r\n _logger.LogWarning(\"⚠ Max turns reached - stopping conversation\");\r\n }\r\n\r\n _logger.LogInformation(\"\\n=== Agentic Loop Complete ===\");\r\n }\r\n\r\n // ===== Helper Methods =====\r\n\r\n /// <summary>\r\n /// Executes a single tool call and returns the result message.\r\n /// </summary>\r\n private async Task<ToolCallResultMessage> ExecuteToolCallAsync(\r\n ToolCallMessage toolCall,\r\n IDictionary<string, ToolHandler> handlers\r\n )\r\n {\r\n var toolCallId = toolCall.ToolCallId ?? $\"call_{toolCall.Index}\";\r\n var functionName = toolCall.FunctionName ?? \"unknown\";\r\n var functionArgs = toolCall.FunctionArgs ?? \"{}\";\r\n\r\n try\r\n {\r\n if (handlers.TryGetValue(functionName, out var handler))\r\n {\r\n var ctx = new ToolCallContext { ToolCallId = toolCall.ToolCallId };\r\n var result = await handler(functionArgs, ctx, CancellationToken.None);\r\n return new ToolCallResultMessage\r\n {\r\n ToolCallId = toolCallId,\r\n Result = result.ResultText,\r\n Role = Role.User,\r\n FromAgent = toolCall.FromAgent,\r\n GenerationId = toolCall.GenerationId,\r\n };\r\n }\r\n else\r\n {\r\n _logger.LogWarning(\"No handler found for function: {FunctionName}\", functionName);\r\n return new ToolCallResultMessage\r\n {\r\n ToolCallId = toolCallId,\r\n Result = JsonSerializer.Serialize(new { error = $\"Unknown function: {functionName}\" }),\r\n Role = Role.User,\r\n FromAgent = toolCall.FromAgent,\r\n GenerationId = toolCall.GenerationId,\r\n };\r\n }\r\n }\r\n catch (Exception ex)\r\n {\r\n _logger.LogError(ex, \"Error executing tool call: {FunctionName}\", functionName);\r\n return new ToolCallResultMessage\r\n {\r\n ToolCallId = toolCallId,\r\n Result = JsonSerializer.Serialize(new { error = ex.Message }),\r\n Role = Role.User,\r\n FromAgent = toolCall.FromAgent,\r\n GenerationId = toolCall.GenerationId,\r\n };\r\n }\r\n }\r\n\r\n // ===== Helper Classes =====\r\n\r\n private class WeatherArgs\r\n {\r\n public string? Location { get; set; }\r\n public string? Unit { get; set; }\r\n }\r\n\r\n private class TimeArgs\r\n {\r\n public string? Timezone { get; set; }\r\n }\r\n\r\n private class CalculateArgs\r\n {\r\n public string? Expression { get; set; }\r\n }\r\n\r\n // Simple expression evaluator (for demo purposes only - unsafe in production)\r\n private static double EvaluateSimpleExpression(string expression)\r\n {\r\n expression = expression.Replace(\" \", \"\");\r\n\r\n // Very basic calculator - only handles +, -, *, /\r\n if (expression.Contains('+'))\r\n {\r\n var parts = expression.Split('+');\r\n return double.Parse(parts[0]) + double.Parse(parts[1]);\r\n }\r\n if (expression.Contains('-'))\r\n {\r\n var parts = expression.Split('-');\r\n return double.Parse(parts[0]) - double.Parse(parts[1]);\r\n }\r\n if (expression.Contains('*'))\r\n {\r\n var parts = expression.Split('*');\r\n return double.Parse(parts[0]) * double.Parse(parts[1]);\r\n }\r\n if (expression.Contains('/'))\r\n {\r\n var parts = expression.Split('/');\r\n return double.Parse(parts[0]) / double.Parse(parts[1]);\r\n }\r\n\r\n return double.Parse(expression);\r\n }\r\n}\r\n"}, "f7662cabb85b75d89939f274482822aa22c3113f": {"t": 1785756731.7211182, "n": 18060, "h": "b06dc06feecc4a8b", "c": "using System.Text.Json;\r\nusing AchieveAi.LmDotnetTools.LmConfig.Agents;\r\nusing AchieveAi.LmDotnetTools.LmCore.Core;\r\nusing AchieveAi.LmDotnetTools.LmCore.Messages;\r\nusing AchieveAi.LmDotnetTools.LmCore.Middleware;\r\nusing AchieveAi.LmDotnetTools.LmCore.Utils;\r\nusing Microsoft.Extensions.Logging;\r\n\r\nnamespace LmConfigUsageExample;\r\n\r\n/// <summary>\r\n/// Example demonstrating an agentic loop using OpenAI provider connected to Grok4.1\r\n/// with the new middleware chain architecture.\r\n///\r\n/// Middleware Chain:\r\n/// Provider Agent => MessageTransformationMiddleware => JsonFragmentUpdateMiddleware\r\n/// => MessageUpdateJoinerMiddleware => ToolCallInjectionMiddleware\r\n///\r\n/// Agentic Loop:\r\n/// 1. Call middleware chain (streaming)\r\n/// 2. Collect all messages from the stream\r\n/// 3. Find ToolsCallMessage in the messages\r\n/// 4. If found, use ToolCallExecutor to execute tool calls asynchronously\r\n/// 5. Add ToolsCallResultMessage to conversation\r\n/// 6. Loop again\r\n/// 7. If no ToolsCallMessage, end the loop\r\n/// </summary>\r\npublic class OpenAiGrokAgenticExample\r\n{\r\n private readonly IModelResolver _modelResolver;\r\n private readonly IProviderAgentFactory _agentFactory;\r\n private readonly ILogger<OpenAiGrokAgenticExample> _logger;\r\n private static readonly string[] stringArray = [\"sunny\", \"cloudy\", \"rainy\", \"partly cloudy\"];\r\n\r\n public OpenAiGrokAgenticExample(\r\n IModelResolver modelResolver,\r\n IProviderAgentFactory agentFactory,\r\n ILogger<OpenAiGrokAgenticExample> logger\r\n )\r\n {\r\n _modelResolver = modelResolver;\r\n _agentFactory = agentFactory;\r\n _logger = logger;\r\n }\r\n\r\n public async Task RunAsync(string prompt, string modelId = \"x-ai/grok-4.1-fast\", float temperature = 0.7f, int maxTurns = 10)\r\n {\r\n _logger.LogInformation(\"=== Agentic Loop Example with {ModelId} ===\\n\", modelId);\r\n\r\n // ===== Step 1: Resolve the specified model =====\r\n var resolution = await _modelResolver.ResolveProviderAsync(modelId);\r\n if (resolution == null)\r\n {\r\n _logger.LogError(\"Failed to resolve model: {ModelId}\", modelId);\r\n _logger.LogWarning(\"Make sure the model exists in models.json and the provider has an API key set.\");\r\n _logger.LogWarning(\"\");\r\n _logger.LogWarning(\"Required API keys by model:\");\r\n _logger.LogWarning(\" grok-4.1, x-ai/* -> XAI_API_KEY\");\r\n _logger.LogWarning(\" gpt-4.1*, openai/* -> OPENAI_API_KEY\");\r\n _logger.LogWarning(\" claude-3-*, anthropic/* -> ANTHROPIC_API_KEY\");\r\n _logger.LogWarning(\" openrouter/* -> OPENROUTER_API_KEY\");\r\n _logger.LogWarning(\" deepseek/* -> DEEPSEEK_API_KEY\");\r\n _logger.LogWarning(\" claude-sonnet-4-5 -> ClaudeAgentSDK (no API key needed if Claude Code authenticated)\");\r\n _logger.LogWarning(\"\");\r\n _logger.LogWarning(\"Use '--list-providers' to see provider status.\");\r\n return;\r\n }\r\n\r\n _logger.LogInformation(\"✓ Resolved model: {ModelName}\", resolution.EffectiveModelName);\r\n _logger.LogInformation(\" Provider: {ProviderName}\", resolution.EffectiveProviderName);\r\n _logger.LogInformation(\" Endpoint: {Endpoint}\\n\", resolution.Connection.EndpointUrl);\r\n\r\n // ===== Step 3: Define Tools using FunctionRegistry =====\r\n var registry = new FunctionRegistry();\r\n\r\n // get_weather\r\n _ = registry.AddFunction(\r\n new FunctionContract\r\n {\r\n Name = \"get_weather\",\r\n Description = \"Get the current weather for a location\",\r\n Parameters =\r\n [\r\n new FunctionParameterContract\r\n {\r\n Name = \"location\",\r\n Description = \"The city name (e.g., 'San Francisco', 'New York')\",\r\n ParameterType = SchemaHelper.CreateJsonSchemaFromType(typeof(string)),\r\n IsRequired = true,\r\n },\r\n new FunctionParameterContract\r\n {\r\n Name = \"unit\",\r\n Description = \"Temperature unit: 'celsius' or 'fahrenheit'\",\r\n ParameterType = SchemaHelper.CreateJsonSchemaFromType(typeof(string)),\r\n IsRequired = false,\r\n },\r\n ],\r\n },\r\n async (args, _, _) =>\r\n {\r\n var weatherArgs = JsonSerializer.Deserialize<WeatherArgs>(args);\r\n _logger.LogInformation(\"[TOOL] Getting weather for {Location}\", weatherArgs?.Location);\r\n\r\n await Task.Delay(100); // Simulate API call\r\n\r\n var weather = new\r\n {\r\n location = weatherArgs?.Location,\r\n temperature = Random.Shared.Next(60, 85),\r\n unit = weatherArgs?.Unit ?? \"fahrenheit\",\r\n condition = stringArray[Random.Shared.Next(4)],\r\n };\r\n\r\n return JsonSerializer.Serialize(weather);\r\n },\r\n providerName: \"Example\"\r\n );\r\n\r\n // get_time\r\n _ = registry.AddFunction(\r\n new FunctionContract\r\n {\r\n Name = \"get_time\",\r\n Description = \"Get the current time for a timezone\",\r\n Parameters =\r\n [\r\n new FunctionParameterContract\r\n {\r\n Name = \"timezone\",\r\n Description = \"Timezone (e.g., 'PST', 'EST', 'UTC')\",\r\n ParameterType = SchemaHelper.CreateJsonSchemaFromType(typeof(string)),\r\n IsRequired = true,\r\n },\r\n ],\r\n },\r\n async (args, _, _) =>\r\n {\r\n var timeArgs = JsonSerializer.Deserialize<TimeArgs>(args);\r\n _logger.LogInformation(\"[TOOL] Getting time for {Timezone}\", timeArgs?.Timezone);\r\n\r\n await Task.Delay(100); // Simulate API call\r\n\r\n var time = new\r\n {\r\n timezone = timeArgs?.Timezone,\r\n time = DateTime.UtcNow.ToString(\"HH:mm:ss\"),\r\n date = DateTime.UtcNow.ToString(\"yyyy-MM-dd\"),\r\n };\r\n\r\n return JsonSerializer.Serialize(time);\r\n },\r\n providerName: \"Example\"\r\n );\r\n\r\n // calculate\r\n _ = registry.AddFunction(\r\n new FunctionContract\r\n {\r\n Name = \"calculate\",\r\n Description = \"Perform a mathematical calculation\",\r\n Parameters =\r\n [\r\n new FunctionParameterContract\r\n {\r\n Name = \"expression\",\r\n Description = \"Mathematical expression (e.g., '2 + 2', '10 * 5')\",\r\n ParameterType = SchemaHelper.CreateJsonSchemaFromType(typeof(string)),\r\n IsRequired = true,\r\n },\r\n ],\r\n },\r\n async (args, _, _) =>\r\n {\r\n var calcArgs = JsonSerializer.Deserialize<CalculateArgs>(args);\r\n _logger.LogInformation(\"[TOOL] Calculating: {Expression}\", calcArgs?.Expression);\r\n\r\n await Task.Delay(50); // Simulate processing\r\n\r\n // Simple eval-like calculation (for demo purposes only - unsafe in production)\r\n try\r\n {\r\n var result = EvaluateSimpleExpression(calcArgs?.Expression ?? \"0\");\r\n return JsonSerializer.Serialize(new { result, expression = calcArgs?.Expression });\r\n }\r\n catch (Exception ex)\r\n {\r\n return JsonSerializer.Serialize(new { error = ex.Message });\r\n }\r\n },\r\n providerName: \"Example\"\r\n );\r\n\r\n // Build middleware and handlers from registry\r\n var (toolCallMiddleware, functionHandlers) = registry.BuildToolCallComponents(name: \"ToolCallInjection\");\r\n\r\n // ===== Step 4: Create Provider Agent =====\r\n var providerAgent = _agentFactory.CreateStreamingAgent(resolution)\r\n .WithMessageTransformation()\r\n .WithMiddleware(new JsonFragmentUpdateMiddleware())\r\n .WithMiddleware(new MessageUpdateJoinerMiddleware(name: \"MessageJoiner\"))\r\n .WithMiddleware(toolCallMiddleware);\r\n\r\n // ===== Step 6: Start Conversation =====\r\n var conversationHistory = new List<IMessage>\r\n {\r\n new TextMessage { Text = prompt, Role = Role.User },\r\n };\r\n\r\n _logger.LogInformation(\"User: {Prompt}\\n\", prompt);\r\n\r\n // ===== Step 7: Agentic Loop =====\r\n var turnCount = 0;\r\n\r\n while (turnCount < maxTurns)\r\n {\r\n turnCount++;\r\n _logger.LogInformation(\"=== Turn {Turn} ===\", turnCount);\r\n\r\n // Call LLM through middleware chain (streaming)\r\n var streamTask = await providerAgent.GenerateReplyStreamingAsync(\r\n conversationHistory,\r\n new GenerateReplyOptions\r\n {\r\n ModelId = modelId,\r\n // Temperature = temperature,\r\n RunId = Guid.NewGuid().ToString(),\r\n ParentRunId = null,\r\n ThreadId = Guid.NewGuid().ToString(),\r\n }\r\n );\r\n\r\n // Collect messages from stream and execute tool calls in parallel as they arrive\r\n var messages = new List<IMessage>();\r\n var textContent = new System.Text.StringBuilder();\r\n var pendingToolCalls = new Dictionary<string, Task<ToolCallResultMessage>>();\r\n\r\n await foreach (var message in streamTask)\r\n {\r\n messages.Add(message);\r\n\r\n // Display streaming content in real-time and handle tool calls\r\n switch (message)\r\n {\r\n case ReasoningMessage reasoningMsg:\r\n if (!string.IsNullOrEmpty(reasoningMsg.Reasoning))\r\n {\r\n Console.Write(reasoningMsg.Reasoning);\r\n _ = textContent.Append(reasoningMsg.Reasoning);\r\n }\r\n break;\r\n\r\n case TextMessage textMsg:\r\n if (!string.IsNullOrEmpty(textMsg.Text))\r\n {\r\n Console.Write(textMsg.Text);\r\n _ = textContent.Append(textMsg.Text);\r\n }\r\n break;\r\n\r\n case ToolCallMessage toolCall:\r\n // Start executing tool call immediately when we receive it\r\n _logger.LogInformation(\r\n \"[Tool Call] Starting execution: {FunctionName} (id: {ToolCallId})\",\r\n toolCall.FunctionName,\r\n toolCall.ToolCallId\r\n );\r\n\r\n var executionTask = ExecuteToolCallAsync(toolCall, functionHandlers);\r\n pendingToolCalls[toolCall.ToolCallId ?? $\"call_{pendingToolCalls.Count}\"] = executionTask;\r\n break;\r\n\r\n case UsageMessage usage:\r\n _logger.LogDebug(\r\n \"[Usage] Prompt: {Prompt}, Completion: {Completion}\",\r\n usage.Usage.PromptTokens,\r\n usage.Usage.CompletionTokens\r\n );\r\n break;\r\n\r\n default:\r\n // Ignore other message types\r\n break;\r\n }\r\n }\r\n\r\n Console.WriteLine(); // New line after streaming\r\n\r\n // Add all messages to conversation history\r\n conversationHistory.AddRange(messages);\r\n\r\n _logger.LogInformation(\"Received {Count} message(s)\", messages.Count);\r\n\r\n // ===== Step 8: Check for Tool Calls and Await Results =====\r\n if (pendingToolCalls.Count == 0)\r\n {\r\n // No tool calls, conversation complete\r\n _logger.LogInformation(\"✓ No tool calls - conversation complete\\n\");\r\n\r\n if (textContent.Length > 0)\r\n {\r\n _logger.LogInformation(\"Assistant: {Response}\", textContent.ToString());\r\n }\r\n\r\n break;\r\n }\r\n\r\n // ===== Step 9: Await Tool Results and Add to Conversation =====\r\n _logger.LogInformation(\"Awaiting {Count} tool call result(s)...\", pendingToolCalls.Count);\r\n\r\n try\r\n {\r\n // Await all pending tool executions\r\n _ = await Task.WhenAll(pendingToolCalls.Values);\r\n\r\n // Collect results and add to conversation\r\n foreach (var kvp in pendingToolCalls)\r\n {\r\n var result = await kvp.Value;\r\n var resultPreview = result.Result.Length > 100 ? result.Result[..100] + \"...\" : result.Result;\r\n _logger.LogInformation(\" Result for {ToolCallId}: {Result}\", kvp.Key, resultPreview);\r\n\r\n // Add each tool result to conversation history\r\n conversationHistory.Add(result);\r\n }\r\n\r\n _logger.LogInformation(\"Tool execution completed.\");\r\n Console.WriteLine(); // Spacing\r\n }\r\n catch (Exception ex)\r\n {\r\n _logger.LogError(ex, \"Tool execution failed\");\r\n\r\n // Add error message to conversation\r\n var errorMessage = new TextMessage { Text = $\"Error executing tools: {ex.Message}\", Role = Role.User };\r\n conversationHistory.Add(errorMessage);\r\n }\r\n }\r\n\r\n if (turnCount >= maxTurns)\r\n {\r\n _logger.LogWarning(\"⚠ Max turns reached - stopping conversation\");\r\n }\r\n\r\n _logger.LogInformation(\"\\n=== Agentic Loop Complete ===\");\r\n }\r\n\r\n // ===== Helper Methods =====\r\n\r\n /// <summary>\r\n /// Executes a single tool call and returns the result message.\r\n /// </summary>\r\n private async Task<ToolCallResultMessage> ExecuteToolCallAsync(\r\n ToolCallMessage toolCall,\r\n IDictionary<string, ToolHandler> handlers\r\n )\r\n {\r\n var toolCallId = toolCall.ToolCallId ?? $\"call_{toolCall.Index}\";\r\n var functionName = toolCall.FunctionName ?? \"unknown\";\r\n var functionArgs = toolCall.FunctionArgs ?? \"{}\";\r\n\r\n try\r\n {\r\n if (handlers.TryGetValue(functionName, out var handler))\r\n {\r\n var ctx = new ToolCallContext { ToolCallId = toolCall.ToolCallId };\r\n var result = await handler(functionArgs, ctx, CancellationToken.None);\r\n return new ToolCallResultMessage\r\n {\r\n ToolCallId = toolCallId,\r\n Result = result.ResultText,\r\n Role = Role.User,\r\n FromAgent = toolCall.FromAgent,\r\n GenerationId = toolCall.GenerationId,\r\n };\r\n }\r\n else\r\n {\r\n _logger.LogWarning(\"No handler found for function: {FunctionName}\", functionName);\r\n return new ToolCallResultMessage\r\n {\r\n ToolCallId = toolCallId,\r\n Result = JsonSerializer.Serialize(new { error = $\"Unknown function: {functionName}\" }),\r\n Role = Role.User,\r\n FromAgent = toolCall.FromAgent,\r\n GenerationId = toolCall.GenerationId,\r\n };\r\n }\r\n }\r\n catch (Exception ex)\r\n {\r\n _logger.LogError(ex, \"Error executing tool call: {FunctionName}\", functionName);\r\n return new ToolCallResultMessage\r\n {\r\n ToolCallId = toolCallId,\r\n Result = JsonSerializer.Serialize(new { error = ex.Message }),\r\n Role = Role.User,\r\n FromAgent = toolCall.FromAgent,\r\n GenerationId = toolCall.GenerationId,\r\n };\r\n }\r\n }\r\n\r\n // ===== Helper Classes =====\r\n\r\n private class WeatherArgs\r\n {\r\n public string? Location { get; set; }\r\n public string? Unit { get; set; }\r\n }\r\n\r\n private class TimeArgs\r\n {\r\n public string? Timezone { get; set; }\r\n }\r\n\r\n private class CalculateArgs\r\n {\r\n public string? Expression { get; set; }\r\n }\r\n\r\n // Simple expression evaluator (for demo purposes only - unsafe in production)\r\n private static double EvaluateSimpleExpression(string expression)\r\n {\r\n expression = expression.Replace(\" \", \"\");\r\n\r\n // Very basic calculator - only handles +, -, *, /\r\n if (expression.Contains('+'))\r\n {\r\n var parts = expression.Split('+');\r\n return double.Parse(parts[0]) + double.Parse(parts[1]);\r\n }\r\n if (expression.Contains('-'))\r\n {\r\n var parts = expression.Split('-');\r\n return double.Parse(parts[0]) - double.Parse(parts[1]);\r\n }\r\n if (expression.Contains('*'))\r\n {\r\n var parts = expression.Split('*');\r\n return double.Parse(parts[0]) * double.Parse(parts[1]);\r\n }\r\n if (expression.Contains('/'))\r\n {\r\n var parts = expression.Split('/');\r\n return double.Parse(parts[0]) / double.Parse(parts[1]);\r\n }\r\n\r\n return double.Parse(expression);\r\n }\r\n}\r\n"}, "274826859b8a5fca8b68a12e8d83e762390ca0aa": {"t": 1785756733.3940406, "n": 18027, "h": "25ca1fc6d9afe726", "c": "using System.Text.Json;\r\nusing AchieveAi.LmDotnetTools.LmConfig.Agents;\r\nusing AchieveAi.LmDotnetTools.LmCore.Core;\r\nusing AchieveAi.LmDotnetTools.LmCore.Messages;\r\nusing AchieveAi.LmDotnetTools.LmCore.Middleware;\r\nusing AchieveAi.LmDotnetTools.LmCore.Utils;\r\nusing Microsoft.Extensions.Logging;\r\n\r\nnamespace LmConfigUsageExample;\r\n\r\n/// <summary>\r\n/// Example demonstrating an agentic loop using OpenAI provider connected to Grok4.1\r\n/// with the new middleware chain architecture.\r\n///\r\n/// Middleware Chain:\r\n/// Provider Agent => MessageTransformationMiddleware => JsonFragmentUpdateMiddleware\r\n/// => MessageUpdateJoinerMiddleware => ToolCallInjectionMiddleware\r\n///\r\n/// Agentic Loop:\r\n/// 1. Call middleware chain (streaming)\r\n/// 2. Collect all messages from the stream\r\n/// 3. Find ToolsCallMessage in the messages\r\n/// 4. If found, use ToolCallExecutor to execute tool calls asynchronously\r\n/// 5. Add ToolsCallResultMessage to conversation\r\n/// 6. Loop again\r\n/// 7. If no ToolsCallMessage, end the loop\r\n/// </summary>\r\npublic class OpenAiGrokAgenticExample\r\n{\r\n private readonly IModelResolver _modelResolver;\r\n private readonly IProviderAgentFactory _agentFactory;\r\n private readonly ILogger<OpenAiGrokAgenticExample> _logger;\r\n private static readonly string[] stringArray = [\"sunny\", \"cloudy\", \"rainy\", \"partly cloudy\"];\r\n\r\n public OpenAiGrokAgenticExample(\r\n IModelResolver modelResolver,\r\n IProviderAgentFactory agentFactory,\r\n ILogger<OpenAiGrokAgenticExample> logger\r\n )\r\n {\r\n _modelResolver = modelResolver;\r\n _agentFactory = agentFactory;\r\n _logger = logger;\r\n }\r\n\r\n public async Task RunAsync(string prompt, string modelId = \"x-ai/grok-4.1-fast\", float temperature = 0.7f, int maxTurns = 10)\r\n {\r\n _logger.LogInformation(\"=== Agentic Loop Example with {ModelId} ===\\n\", modelId);\r\n\r\n // ===== Step 1: Resolve the specified model =====\r\n var resolution = await _modelResolver.ResolveProviderAsync(modelId);\r\n if (resolution == null)\r\n {\r\n _logger.LogError(\"Failed to resolve model: {ModelId}\", modelId);\r\n _logger.LogWarning(\"Make sure the model exists in models.json and the provider has an API key set.\");\r\n _logger.LogWarning(\"\");\r\n _logger.LogWarning(\"Required API keys by model:\");\r\n _logger.LogWarning(\" grok-4.1, x-ai/* -> XAI_API_KEY\");\r\n _logger.LogWarning(\" gpt-4.1*, openai/* -> OPENAI_API_KEY\");\r\n _logger.LogWarning(\" claude-3-*, anthropic/* -> ANTHROPIC_API_KEY\");\r\n _logger.LogWarning(\" openrouter/* -> OPENROUTER_API_KEY\");\r\n _logger.LogWarning(\" deepseek/* -> DEEPSEEK_API_KEY\");\r\n _logger.LogWarning(\" claude-sonnet-4-5 -> ClaudeAgentSDK (no API key needed if Claude Code authenticated)\");\r\n _logger.LogWarning(\"\");\r\n _logger.LogWarning(\"Use '--list-providers' to see provider status.\");\r\n return;\r\n }\r\n\r\n _logger.LogInformation(\"✓ Resolved model: {ModelName}\", resolution.EffectiveModelName);\r\n _logger.LogInformation(\" Provider: {ProviderName}\", resolution.EffectiveProviderName);\r\n _logger.LogInformation(\" Endpoint: {Endpoint}\\n\", resolution.Connection.EndpointUrl);\r\n\r\n // ===== Step 3: Define Tools using FunctionRegistry =====\r\n var registry = new FunctionRegistry();\r\n\r\n // get_weather\r\n _ = registry.AddFunction(\r\n new FunctionContract\r\n {\r\n Name = \"get_weather\",\r\n Description = \"Get the current weather for a location\",\r\n Parameters =\r\n [\r\n new FunctionParameterContract\r\n {\r\n Name = \"location\",\r\n Description = \"The city name (e.g., 'San Francisco', 'New York')\",\r\n ParameterType = SchemaHelper.CreateJsonSchemaFromType(typeof(string)),\r\n IsRequired = true,\r\n },\r\n new FunctionParameterContract\r\n {\r\n Name = \"unit\",\r\n Description = \"Temperature unit: 'celsius' or 'fahrenheit'\",\r\n ParameterType = SchemaHelper.CreateJsonSchemaFromType(typeof(string)),\r\n IsRequired = false,\r\n },\r\n ],\r\n },\r\n async (args, _) =>\r\n {\r\n var weatherArgs = JsonSerializer.Deserialize<WeatherArgs>(args);\r\n _logger.LogInformation(\"[TOOL] Getting weather for {Location}\", weatherArgs?.Location);\r\n\r\n await Task.Delay(100); // Simulate API call\r\n\r\n var weather = new\r\n {\r\n location = weatherArgs?.Location,\r\n temperature = Random.Shared.Next(60, 85),\r\n unit = weatherArgs?.Unit ?? \"fahrenheit\",\r\n condition = stringArray[Random.Shared.Next(4)],\r\n };\r\n\r\n return JsonSerializer.Serialize(weather);\r\n },\r\n providerName: \"Example\"\r\n );\r\n\r\n // get_time\r\n _ = registry.AddFunction(\r\n new FunctionContract\r\n {\r\n Name = \"get_time\",\r\n Description = \"Get the current time for a timezone\",\r\n Parameters =\r\n [\r\n new FunctionParameterContract\r\n {\r\n Name = \"timezone\",\r\n Description = \"Timezone (e.g., 'PST', 'EST', 'UTC')\",\r\n ParameterType = SchemaHelper.CreateJsonSchemaFromType(typeof(string)),\r\n IsRequired = true,\r\n },\r\n ],\r\n },\r\n async (args, _) =>\r\n {\r\n var timeArgs = JsonSerializer.Deserialize<TimeArgs>(args);\r\n _logger.LogInformation(\"[TOOL] Getting time for {Timezone}\", timeArgs?.Timezone);\r\n\r\n await Task.Delay(100); // Simulate API call\r\n\r\n var time = new\r\n {\r\n timezone = timeArgs?.Timezone,\r\n time = DateTime.UtcNow.ToString(\"HH:mm:ss\"),\r\n date = DateTime.UtcNow.ToString(\"yyyy-MM-dd\"),\r\n };\r\n\r\n return JsonSerializer.Serialize(time);\r\n },\r\n providerName: \"Example\"\r\n );\r\n\r\n // calculate\r\n _ = registry.AddFunction(\r\n new FunctionContract\r\n {\r\n Name = \"calculate\",\r\n Description = \"Perform a mathematical calculation\",\r\n Parameters =\r\n [\r\n new FunctionParameterContract\r\n {\r\n Name = \"expression\",\r\n Description = \"Mathematical expression (e.g., '2 + 2', '10 * 5')\",\r\n ParameterType = SchemaHelper.CreateJsonSchemaFromType(typeof(string)),\r\n IsRequired = true,\r\n },\r\n ],\r\n },\r\n async (args, _) =>\r\n {\r\n var calcArgs = JsonSerializer.Deserialize<CalculateArgs>(args);\r\n _logger.LogInformation(\"[TOOL] Calculating: {Expression}\", calcArgs?.Expression);\r\n\r\n await Task.Delay(50); // Simulate processing\r\n\r\n // Simple eval-like calculation (for demo purposes only - unsafe in production)\r\n try\r\n {\r\n var result = EvaluateSimpleExpression(calcArgs?.Expression ?? \"0\");\r\n return JsonSerializer.Serialize(new { result, expression = calcArgs?.Expression });\r\n }\r\n catch (Exception ex)\r\n {\r\n return JsonSerializer.Serialize(new { error = ex.Message });\r\n }\r\n },\r\n providerName: \"Example\"\r\n );\r\n\r\n // Build middleware and handlers from registry\r\n var (toolCallMiddleware, functionHandlers) = registry.BuildToolCallComponents(name: \"ToolCallInjection\");\r\n\r\n // ===== Step 4: Create Provider Agent =====\r\n var providerAgent = _agentFactory.CreateStreamingAgent(resolution)\r\n .WithMessageTransformation()\r\n .WithMiddleware(new JsonFragmentUpdateMiddleware())\r\n .WithMiddleware(new MessageUpdateJoinerMiddleware(name: \"MessageJoiner\"))\r\n .WithMiddleware(toolCallMiddleware);\r\n\r\n // ===== Step 6: Start Conversation =====\r\n var conversationHistory = new List<IMessage>\r\n {\r\n new TextMessage { Text = prompt, Role = Role.User },\r\n };\r\n\r\n _logger.LogInformation(\"User: {Prompt}\\n\", prompt);\r\n\r\n // ===== Step 7: Agentic Loop =====\r\n var turnCount = 0;\r\n\r\n while (turnCount < maxTurns)\r\n {\r\n turnCount++;\r\n _logger.LogInformation(\"=== Turn {Turn} ===\", turnCount);\r\n\r\n // Call LLM through middleware chain (streaming)\r\n var streamTask = await providerAgent.GenerateReplyStreamingAsync(\r\n conversationHistory,\r\n new GenerateReplyOptions\r\n {\r\n ModelId = modelId,\r\n // Temperature = temperature,\r\n RunId = Guid.NewGuid().ToString(),\r\n ParentRunId = null,\r\n ThreadId = Guid.NewGuid().ToString(),\r\n }\r\n );\r\n\r\n // Collect messages from stream and execute tool calls in parallel as they arrive\r\n var messages = new List<IMessage>();\r\n var textContent = new System.Text.StringBuilder();\r\n var pendingToolCalls = new Dictionary<string, Task<ToolCallResultMessage>>();\r\n\r\n await foreach (var message in streamTask)\r\n {\r\n messages.Add(message);\r\n\r\n // Display streaming content in real-time and handle tool calls\r\n switch (message)\r\n {\r\n case ReasoningMessage reasoningMsg:\r\n if (!string.IsNullOrEmpty(reasoningMsg.Reasoning))\r\n {\r\n Console.Write(reasoningMsg.Reasoning);\r\n _ = textContent.Append(reasoningMsg.Reasoning);\r\n }\r\n break;\r\n\r\n case TextMessage textMsg:\r\n if (!string.IsNullOrEmpty(textMsg.Text))\r\n {\r\n Console.Write(textMsg.Text);\r\n _ = textContent.Append(textMsg.Text);\r\n }\r\n break;\r\n\r\n case ToolCallMessage toolCall:\r\n // Start executing tool call immediately when we receive it\r\n _logger.LogInformation(\r\n \"[Tool Call] Starting execution: {FunctionName} (id: {ToolCallId})\",\r\n toolCall.FunctionName,\r\n toolCall.ToolCallId\r\n );\r\n\r\n var executionTask = ExecuteToolCallAsync(toolCall, functionHandlers);\r\n pendingToolCalls[toolCall.ToolCallId ?? $\"call_{pendingToolCalls.Count}\"] = executionTask;\r\n break;\r\n\r\n case UsageMessage usage:\r\n _logger.LogDebug(\r\n \"[Usage] Prompt: {Prompt}, Completion: {Completion}\",\r\n usage.Usage.PromptTokens,\r\n usage.Usage.CompletionTokens\r\n );\r\n break;\r\n\r\n default:\r\n // Ignore other message types\r\n break;\r\n }\r\n }\r\n\r\n Console.WriteLine(); // New line after streaming\r\n\r\n // Add all messages to conversation history\r\n conversationHistory.AddRange(messages);\r\n\r\n _logger.LogInformation(\"Received {Count} message(s)\", messages.Count);\r\n\r\n // ===== Step 8: Check for Tool Calls and Await Results =====\r\n if (pendingToolCalls.Count == 0)\r\n {\r\n // No tool calls, conversation complete\r\n _logger.LogInformation(\"✓ No tool calls - conversation complete\\n\");\r\n\r\n if (textContent.Length > 0)\r\n {\r\n _logger.LogInformation(\"Assistant: {Response}\", textContent.ToString());\r\n }\r\n\r\n break;\r\n }\r\n\r\n // ===== Step 9: Await Tool Results and Add to Conversation =====\r\n _logger.LogInformation(\"Awaiting {Count} tool call result(s)...\", pendingToolCalls.Count);\r\n\r\n try\r\n {\r\n // Await all pending tool executions\r\n _ = await Task.WhenAll(pendingToolCalls.Values);\r\n\r\n // Collect results and add to conversation\r\n foreach (var kvp in pendingToolCalls)\r\n {\r\n var result = await kvp.Value;\r\n var resultPreview = result.Result.Length > 100 ? result.Result[..100] + \"...\" : result.Result;\r\n _logger.LogInformation(\" Result for {ToolCallId}: {Result}\", kvp.Key, resultPreview);\r\n\r\n // Add each tool result to conversation history\r\n conversationHistory.Add(result);\r\n }\r\n\r\n _logger.LogInformation(\"Tool execution completed.\");\r\n Console.WriteLine(); // Spacing\r\n }\r\n catch (Exception ex)\r\n {\r\n _logger.LogError(ex, \"Tool execution failed\");\r\n\r\n // Add error message to conversation\r\n var errorMessage = new TextMessage { Text = $\"Error executing tools: {ex.Message}\", Role = Role.User };\r\n conversationHistory.Add(errorMessage);\r\n }\r\n }\r\n\r\n if (turnCount >= maxTurns)\r\n {\r\n _logger.LogWarning(\"⚠ Max turns reached - stopping conversation\");\r\n }\r\n\r\n _logger.LogInformation(\"\\n=== Agentic Loop Complete ===\");\r\n }\r\n\r\n // ===== Helper Methods =====\r\n\r\n /// <summary>\r\n /// Executes a single tool call and returns the result message.\r\n /// </summary>\r\n private async Task<ToolCallResultMessage> ExecuteToolCallAsync(\r\n ToolCallMessage toolCall,\r\n IDictionary<string, ToolHandler> handlers\r\n )\r\n {\r\n var toolCallId = toolCall.ToolCallId ?? $\"call_{toolCall.Index}\";\r\n var functionName = toolCall.FunctionName ?? \"unknown\";\r\n var functionArgs = toolCall.FunctionArgs ?? \"{}\";\r\n\r\n try\r\n {\r\n if (handlers.TryGetValue(functionName, out var handler))\r\n {\r\n var ctx = new ToolCallContext { ToolCallId = toolCall.ToolCallId };\r\n var result = await handler(functionArgs, ctx);\r\n return new ToolCallResultMessage\r\n {\r\n ToolCallId = toolCallId,\r\n Result = result.ResultText,\r\n Role = Role.User,\r\n FromAgent = toolCall.FromAgent,\r\n GenerationId = toolCall.GenerationId,\r\n };\r\n }\r\n else\r\n {\r\n _logger.LogWarning(\"No handler found for function: {FunctionName}\", functionName);\r\n return new ToolCallResultMessage\r\n {\r\n ToolCallId = toolCallId,\r\n Result = JsonSerializer.Serialize(new { error = $\"Unknown function: {functionName}\" }),\r\n Role = Role.User,\r\n FromAgent = toolCall.FromAgent,\r\n GenerationId = toolCall.GenerationId,\r\n };\r\n }\r\n }\r\n catch (Exception ex)\r\n {\r\n _logger.LogError(ex, \"Error executing tool call: {FunctionName}\", functionName);\r\n return new ToolCallResultMessage\r\n {\r\n ToolCallId = toolCallId,\r\n Result = JsonSerializer.Serialize(new { error = ex.Message }),\r\n Role = Role.User,\r\n FromAgent = toolCall.FromAgent,\r\n GenerationId = toolCall.GenerationId,\r\n };\r\n }\r\n }\r\n\r\n // ===== Helper Classes =====\r\n\r\n private class WeatherArgs\r\n {\r\n public string? Location { get; set; }\r\n public string? Unit { get; set; }\r\n }\r\n\r\n private class TimeArgs\r\n {\r\n public string? Timezone { get; set; }\r\n }\r\n\r\n private class CalculateArgs\r\n {\r\n public string? Expression { get; set; }\r\n }\r\n\r\n // Simple expression evaluator (for demo purposes only - unsafe in production)\r\n private static double EvaluateSimpleExpression(string expression)\r\n {\r\n expression = expression.Replace(\" \", \"\");\r\n\r\n // Very basic calculator - only handles +, -, *, /\r\n if (expression.Contains('+'))\r\n {\r\n var parts = expression.Split('+');\r\n return double.Parse(parts[0]) + double.Parse(parts[1]);\r\n }\r\n if (expression.Contains('-'))\r\n {\r\n var parts = expression.Split('-');\r\n return double.Parse(parts[0]) - double.Parse(parts[1]);\r\n }\r\n if (expression.Contains('*'))\r\n {\r\n var parts = expression.Split('*');\r\n return double.Parse(parts[0]) * double.Parse(parts[1]);\r\n }\r\n if (expression.Contains('/'))\r\n {\r\n var parts = expression.Split('/');\r\n return double.Parse(parts[0]) / double.Parse(parts[1]);\r\n }\r\n\r\n return double.Parse(expression);\r\n }\r\n}\r\n"}, "5e0ab63291333a85068466185c71ee6d9d2a9c64": {"t": 1785756734.7543228, "n": 17954, "h": "4df478a19f30dcdf", "c": "using System.Text.Json;\r\nusing AchieveAi.LmDotnetTools.LmConfig.Agents;\r\nusing AchieveAi.LmDotnetTools.LmCore.Core;\r\nusing AchieveAi.LmDotnetTools.LmCore.Messages;\r\nusing AchieveAi.LmDotnetTools.LmCore.Middleware;\r\nusing AchieveAi.LmDotnetTools.LmCore.Utils;\r\nusing Microsoft.Extensions.Logging;\r\n\r\nnamespace LmConfigUsageExample;\r\n\r\n/// <summary>\r\n/// Example demonstrating an agentic loop using OpenAI provider connected to Grok4.1\r\n/// with the new middleware chain architecture.\r\n///\r\n/// Middleware Chain:\r\n/// Provider Agent => MessageTransformationMiddleware => JsonFragmentUpdateMiddleware\r\n/// => MessageUpdateJoinerMiddleware => ToolCallInjectionMiddleware\r\n///\r\n/// Agentic Loop:\r\n/// 1. Call middleware chain (streaming)\r\n/// 2. Collect all messages from the stream\r\n/// 3. Find ToolsCallMessage in the messages\r\n/// 4. If found, use ToolCallExecutor to execute tool calls asynchronously\r\n/// 5. Add ToolsCallResultMessage to conversation\r\n/// 6. Loop again\r\n/// 7. If no ToolsCallMessage, end the loop\r\n/// </summary>\r\npublic class OpenAiGrokAgenticExample\r\n{\r\n private readonly IModelResolver _modelResolver;\r\n private readonly IProviderAgentFactory _agentFactory;\r\n private readonly ILogger<OpenAiGrokAgenticExample> _logger;\r\n private static readonly string[] stringArray = [\"sunny\", \"cloudy\", \"rainy\", \"partly cloudy\"];\r\n\r\n public OpenAiGrokAgenticExample(\r\n IModelResolver modelResolver,\r\n IProviderAgentFactory agentFactory,\r\n ILogger<OpenAiGrokAgenticExample> logger\r\n )\r\n {\r\n _modelResolver = modelResolver;\r\n _agentFactory = agentFactory;\r\n _logger = logger;\r\n }\r\n\r\n public async Task RunAsync(string prompt, string modelId = \"x-ai/grok-4.1-fast\", float temperature = 0.7f, int maxTurns = 10)\r\n {\r\n _logger.LogInformation(\"=== Agentic Loop Example with {ModelId} ===\\n\", modelId);\r\n\r\n // ===== Step 1: Resolve the specified model =====\r\n var resolution = await _modelResolver.ResolveProviderAsync(modelId);\r\n if (resolution == null)\r\n {\r\n _logger.LogError(\"Failed to resolve model: {ModelId}\", modelId);\r\n _logger.LogWarning(\"Make sure the model exists in models.json and the provider has an API key set.\");\r\n _logger.LogWarning(\"\");\r\n _logger.LogWarning(\"Required API keys by model:\");\r\n _logger.LogWarning(\" grok-4.1, x-ai/* -> XAI_API_KEY\");\r\n _logger.LogWarning(\" gpt-4.1*, openai/* -> OPENAI_API_KEY\");\r\n _logger.LogWarning(\" claude-3-*, anthropic/* -> ANTHROPIC_API_KEY\");\r\n _logger.LogWarning(\" openrouter/* -> OPENROUTER_API_KEY\");\r\n _logger.LogWarning(\" deepseek/* -> DEEPSEEK_API_KEY\");\r\n _logger.LogWarning(\" claude-sonnet-4-5 -> ClaudeAgentSDK (no API key needed if Claude Code authenticated)\");\r\n _logger.LogWarning(\"\");\r\n _logger.LogWarning(\"Use '--list-providers' to see provider status.\");\r\n return;\r\n }\r\n\r\n _logger.LogInformation(\"✓ Resolved model: {ModelName}\", resolution.EffectiveModelName);\r\n _logger.LogInformation(\" Provider: {ProviderName}\", resolution.EffectiveProviderName);\r\n _logger.LogInformation(\" Endpoint: {Endpoint}\\n\", resolution.Connection.EndpointUrl);\r\n\r\n // ===== Step 3: Define Tools using FunctionRegistry =====\r\n var registry = new FunctionRegistry();\r\n\r\n // get_weather\r\n _ = registry.AddFunction(\r\n new FunctionContract\r\n {\r\n Name = \"get_weather\",\r\n Description = \"Get the current weather for a location\",\r\n Parameters =\r\n [\r\n new FunctionParameterContract\r\n {\r\n Name = \"location\",\r\n Description = \"The city name (e.g., 'San Francisco', 'New York')\",\r\n ParameterType = SchemaHelper.CreateJsonSchemaFromType(typeof(string)),\r\n IsRequired = true,\r\n },\r\n new FunctionParameterContract\r\n {\r\n Name = \"unit\",\r\n Description = \"Temperature unit: 'celsius' or 'fahrenheit'\",\r\n ParameterType = SchemaHelper.CreateJsonSchemaFromType(typeof(string)),\r\n IsRequired = false,\r\n },\r\n ],\r\n },\r\n async (args) =>\r\n {\r\n var weatherArgs = JsonSerializer.Deserialize<WeatherArgs>(args);\r\n _logger.LogInformation(\"[TOOL] Getting weather for {Location}\", weatherArgs?.Location);\r\n\r\n await Task.Delay(100); // Simulate API call\r\n\r\n var weather = new\r\n {\r\n location = weatherArgs?.Location,\r\n temperature = Random.Shared.Next(60, 85),\r\n unit = weatherArgs?.Unit ?? \"fahrenheit\",\r\n condition = stringArray[Random.Shared.Next(4)],\r\n };\r\n\r\n return JsonSerializer.Serialize(weather);\r\n },\r\n providerName: \"Example\"\r\n );\r\n\r\n // get_time\r\n _ = registry.AddFunction(\r\n new FunctionContract\r\n {\r\n Name = \"get_time\",\r\n Description = \"Get the current time for a timezone\",\r\n Parameters =\r\n [\r\n new FunctionParameterContract\r\n {\r\n Name = \"timezone\",\r\n Description = \"Timezone (e.g., 'PST', 'EST', 'UTC')\",\r\n ParameterType = SchemaHelper.CreateJsonSchemaFromType(typeof(string)),\r\n IsRequired = true,\r\n },\r\n ],\r\n },\r\n async (args) =>\r\n {\r\n var timeArgs = JsonSerializer.Deserialize<TimeArgs>(args);\r\n _logger.LogInformation(\"[TOOL] Getting time for {Timezone}\", timeArgs?.Timezone);\r\n\r\n await Task.Delay(100); // Simulate API call\r\n\r\n var time = new\r\n {\r\n timezone = timeArgs?.Timezone,\r\n time = DateTime.UtcNow.ToString(\"HH:mm:ss\"),\r\n date = DateTime.UtcNow.ToString(\"yyyy-MM-dd\"),\r\n };\r\n\r\n return JsonSerializer.Serialize(time);\r\n },\r\n providerName: \"Example\"\r\n );\r\n\r\n // calculate\r\n _ = registry.AddFunction(\r\n new FunctionContract\r\n {\r\n Name = \"calculate\",\r\n Description = \"Perform a mathematical calculation\",\r\n Parameters =\r\n [\r\n new FunctionParameterContract\r\n {\r\n Name = \"expression\",\r\n Description = \"Mathematical expression (e.g., '2 + 2', '10 * 5')\",\r\n ParameterType = SchemaHelper.CreateJsonSchemaFromType(typeof(string)),\r\n IsRequired = true,\r\n },\r\n ],\r\n },\r\n async (args) =>\r\n {\r\n var calcArgs = JsonSerializer.Deserialize<CalculateArgs>(args);\r\n _logger.LogInformation(\"[TOOL] Calculating: {Expression}\", calcArgs?.Expression);\r\n\r\n await Task.Delay(50); // Simulate processing\r\n\r\n // Simple eval-like calculation (for demo purposes only - unsafe in production)\r\n try\r\n {\r\n var result = EvaluateSimpleExpression(calcArgs?.Expression ?? \"0\");\r\n return JsonSerializer.Serialize(new { result, expression = calcArgs?.Expression });\r\n }\r\n catch (Exception ex)\r\n {\r\n return JsonSerializer.Serialize(new { error = ex.Message });\r\n }\r\n },\r\n providerName: \"Example\"\r\n );\r\n\r\n // Build middleware and handlers from registry\r\n var (toolCallMiddleware, functionHandlers) = registry.BuildToolCallComponents(name: \"ToolCallInjection\");\r\n\r\n // ===== Step 4: Create Provider Agent =====\r\n var providerAgent = _agentFactory.CreateStreamingAgent(resolution)\r\n .WithMessageTransformation()\r\n .WithMiddleware(new JsonFragmentUpdateMiddleware())\r\n .WithMiddleware(new MessageUpdateJoinerMiddleware(name: \"MessageJoiner\"))\r\n .WithMiddleware(toolCallMiddleware);\r\n\r\n // ===== Step 6: Start Conversation =====\r\n var conversationHistory = new List<IMessage>\r\n {\r\n new TextMessage { Text = prompt, Role = Role.User },\r\n };\r\n\r\n _logger.LogInformation(\"User: {Prompt}\\n\", prompt);\r\n\r\n // ===== Step 7: Agentic Loop =====\r\n var turnCount = 0;\r\n\r\n while (turnCount < maxTurns)\r\n {\r\n turnCount++;\r\n _logger.LogInformation(\"=== Turn {Turn} ===\", turnCount);\r\n\r\n // Call LLM through middleware chain (streaming)\r\n var streamTask = await providerAgent.GenerateReplyStreamingAsync(\r\n conversationHistory,\r\n new GenerateReplyOptions\r\n {\r\n ModelId = modelId,\r\n // Temperature = temperature,\r\n RunId = Guid.NewGuid().ToString(),\r\n ParentRunId = null,\r\n ThreadId = Guid.NewGuid().ToString(),\r\n }\r\n );\r\n\r\n // Collect messages from stream and execute tool calls in parallel as they arrive\r\n var messages = new List<IMessage>();\r\n var textContent = new System.Text.StringBuilder();\r\n var pendingToolCalls = new Dictionary<string, Task<ToolCallResultMessage>>();\r\n\r\n await foreach (var message in streamTask)\r\n {\r\n messages.Add(message);\r\n\r\n // Display streaming content in real-time and handle tool calls\r\n switch (message)\r\n {\r\n case ReasoningMessage reasoningMsg:\r\n if (!string.IsNullOrEmpty(reasoningMsg.Reasoning))\r\n {\r\n Console.Write(reasoningMsg.Reasoning);\r\n _ = textContent.Append(reasoningMsg.Reasoning);\r\n }\r\n break;\r\n\r\n case TextMessage textMsg:\r\n if (!string.IsNullOrEmpty(textMsg.Text))\r\n {\r\n Console.Write(textMsg.Text);\r\n _ = textContent.Append(textMsg.Text);\r\n }\r\n break;\r\n\r\n case ToolCallMessage toolCall:\r\n // Start executing tool call immediately when we receive it\r\n _logger.LogInformation(\r\n \"[Tool Call] Starting execution: {FunctionName} (id: {ToolCallId})\",\r\n toolCall.FunctionName,\r\n toolCall.ToolCallId\r\n );\r\n\r\n var executionTask = ExecuteToolCallAsync(toolCall, functionHandlers);\r\n pendingToolCalls[toolCall.ToolCallId ?? $\"call_{pendingToolCalls.Count}\"] = executionTask;\r\n break;\r\n\r\n case UsageMessage usage:\r\n _logger.LogDebug(\r\n \"[Usage] Prompt: {Prompt}, Completion: {Completion}\",\r\n usage.Usage.PromptTokens,\r\n usage.Usage.CompletionTokens\r\n );\r\n break;\r\n\r\n default:\r\n // Ignore other message types\r\n break;\r\n }\r\n }\r\n\r\n Console.WriteLine(); // New line after streaming\r\n\r\n // Add all messages to conversation history\r\n conversationHistory.AddRange(messages);\r\n\r\n _logger.LogInformation(\"Received {Count} message(s)\", messages.Count);\r\n\r\n // ===== Step 8: Check for Tool Calls and Await Results =====\r\n if (pendingToolCalls.Count == 0)\r\n {\r\n // No tool calls, conversation complete\r\n _logger.LogInformation(\"✓ No tool calls - conversation complete\\n\");\r\n\r\n if (textContent.Length > 0)\r\n {\r\n _logger.LogInformation(\"Assistant: {Response}\", textContent.ToString());\r\n }\r\n\r\n break;\r\n }\r\n\r\n // ===== Step 9: Await Tool Results and Add to Conversation =====\r\n _logger.LogInformation(\"Awaiting {Count} tool call result(s)...\", pendingToolCalls.Count);\r\n\r\n try\r\n {\r\n // Await all pending tool executions\r\n _ = await Task.WhenAll(pendingToolCalls.Values);\r\n\r\n // Collect results and add to conversation\r\n foreach (var kvp in pendingToolCalls)\r\n {\r\n var result = await kvp.Value;\r\n var resultPreview = result.Result.Length > 100 ? result.Result[..100] + \"...\" : result.Result;\r\n _logger.LogInformation(\" Result for {ToolCallId}: {Result}\", kvp.Key, resultPreview);\r\n\r\n // Add each tool result to conversation history\r\n conversationHistory.Add(result);\r\n }\r\n\r\n _logger.LogInformation(\"Tool execution completed.\");\r\n Console.WriteLine(); // Spacing\r\n }\r\n catch (Exception ex)\r\n {\r\n _logger.LogError(ex, \"Tool execution failed\");\r\n\r\n // Add error message to conversation\r\n var errorMessage = new TextMessage { Text = $\"Error executing tools: {ex.Message}\", Role = Role.User };\r\n conversationHistory.Add(errorMessage);\r\n }\r\n }\r\n\r\n if (turnCount >= maxTurns)\r\n {\r\n _logger.LogWarning(\"⚠ Max turns reached - stopping conversation\");\r\n }\r\n\r\n _logger.LogInformation(\"\\n=== Agentic Loop Complete ===\");\r\n }\r\n\r\n // ===== Helper Methods =====\r\n\r\n /// <summary>\r\n /// Executes a single tool call and returns the result message.\r\n /// </summary>\r\n private async Task<ToolCallResultMessage> ExecuteToolCallAsync(\r\n ToolCallMessage toolCall,\r\n IDictionary<string, Func<string, Task<ToolHandlerResult>>> handlers\r\n )\r\n {\r\n var toolCallId = toolCall.ToolCallId ?? $\"call_{toolCall.Index}\";\r\n var functionName = toolCall.FunctionName ?? \"unknown\";\r\n var functionArgs = toolCall.FunctionArgs ?? \"{}\";\r\n\r\n try\r\n {\r\n if (handlers.TryGetValue(functionName, out var handler))\r\n {\r\n var result = await handler(functionArgs);\r\n return new ToolCallResultMessage\r\n {\r\n ToolCallId = toolCallId,\r\n Result = result.ResultText,\r\n Role = Role.User,\r\n FromAgent = toolCall.FromAgent,\r\n GenerationId = toolCall.GenerationId,\r\n };\r\n }\r\n else\r\n {\r\n _logger.LogWarning(\"No handler found for function: {FunctionName}\", functionName);\r\n return new ToolCallResultMessage\r\n {\r\n ToolCallId = toolCallId,\r\n Result = JsonSerializer.Serialize(new { error = $\"Unknown function: {functionName}\" }),\r\n Role = Role.User,\r\n FromAgent = toolCall.FromAgent,\r\n GenerationId = toolCall.GenerationId,\r\n };\r\n }\r\n }\r\n catch (Exception ex)\r\n {\r\n _logger.LogError(ex, \"Error executing tool call: {FunctionName}\", functionName);\r\n return new ToolCallResultMessage\r\n {\r\n ToolCallId = toolCallId,\r\n Result = JsonSerializer.Serialize(new { error = ex.Message }),\r\n Role = Role.User,\r\n FromAgent = toolCall.FromAgent,\r\n GenerationId = toolCall.GenerationId,\r\n };\r\n }\r\n }\r\n\r\n // ===== Helper Classes =====\r\n\r\n private class WeatherArgs\r\n {\r\n public string? Location { get; set; }\r\n public string? Unit { get; set; }\r\n }\r\n\r\n private class TimeArgs\r\n {\r\n public string? Timezone { get; set; }\r\n }\r\n\r\n private class CalculateArgs\r\n {\r\n public string? Expression { get; set; }\r\n }\r\n\r\n // Simple expression evaluator (for demo purposes only - unsafe in production)\r\n private static double EvaluateSimpleExpression(string expression)\r\n {\r\n expression = expression.Replace(\" \", \"\");\r\n\r\n // Very basic calculator - only handles +, -, *, /\r\n if (expression.Contains('+'))\r\n {\r\n var parts = expression.Split('+');\r\n return double.Parse(parts[0]) + double.Parse(parts[1]);\r\n }\r\n if (expression.Contains('-'))\r\n {\r\n var parts = expression.Split('-');\r\n return double.Parse(parts[0]) - double.Parse(parts[1]);\r\n }\r\n if (expression.Contains('*'))\r\n {\r\n var parts = expression.Split('*');\r\n return double.Parse(parts[0]) * double.Parse(parts[1]);\r\n }\r\n if (expression.Contains('/'))\r\n {\r\n var parts = expression.Split('/');\r\n return double.Parse(parts[0]) / double.Parse(parts[1]);\r\n }\r\n\r\n return double.Parse(expression);\r\n }\r\n}\r\n"}, "7d6fbf36d4f47012f995e306cfb47494fe174abc": {"t": 1785756736.4656126, "n": 17935, "h": "d15755920836bbbd", "c": "using System.Text.Json;\r\nusing AchieveAi.LmDotnetTools.LmConfig.Agents;\r\nusing AchieveAi.LmDotnetTools.LmCore.Core;\r\nusing AchieveAi.LmDotnetTools.LmCore.Messages;\r\nusing AchieveAi.LmDotnetTools.LmCore.Middleware;\r\nusing AchieveAi.LmDotnetTools.LmCore.Utils;\r\nusing Microsoft.Extensions.Logging;\r\n\r\nnamespace LmConfigUsageExample;\r\n\r\n/// <summary>\r\n/// Example demonstrating an agentic loop using OpenAI provider connected to Grok4.1\r\n/// with the new middleware chain architecture.\r\n///\r\n/// Middleware Chain:\r\n/// Provider Agent => MessageTransformationMiddleware => JsonFragmentUpdateMiddleware\r\n/// => MessageUpdateJoinerMiddleware => ToolCallInjectionMiddleware\r\n///\r\n/// Agentic Loop:\r\n/// 1. Call middleware chain (streaming)\r\n/// 2. Collect all messages from the stream\r\n/// 3. Find ToolsCallMessage in the messages\r\n/// 4. If found, use ToolCallExecutor to execute tool calls asynchronously\r\n/// 5. Add ToolsCallResultMessage to conversation\r\n/// 6. Loop again\r\n/// 7. If no ToolsCallMessage, end the loop\r\n/// </summary>\r\npublic class OpenAiGrokAgenticExample\r\n{\r\n private readonly IModelResolver _modelResolver;\r\n private readonly IProviderAgentFactory _agentFactory;\r\n private readonly ILogger<OpenAiGrokAgenticExample> _logger;\r\n private static readonly string[] stringArray = [\"sunny\", \"cloudy\", \"rainy\", \"partly cloudy\"];\r\n\r\n public OpenAiGrokAgenticExample(\r\n IModelResolver modelResolver,\r\n IProviderAgentFactory agentFactory,\r\n ILogger<OpenAiGrokAgenticExample> logger\r\n )\r\n {\r\n _modelResolver = modelResolver;\r\n _agentFactory = agentFactory;\r\n _logger = logger;\r\n }\r\n\r\n public async Task RunAsync(string prompt, string modelId = \"x-ai/grok-4.1-fast\", float temperature = 0.7f, int maxTurns = 10)\r\n {\r\n _logger.LogInformation(\"=== Agentic Loop Example with {ModelId} ===\\n\", modelId);\r\n\r\n // ===== Step 1: Resolve the specified model =====\r\n var resolution = await _modelResolver.ResolveProviderAsync(modelId);\r\n if (resolution == null)\r\n {\r\n _logger.LogError(\"Failed to resolve model: {ModelId}\", modelId);\r\n _logger.LogWarning(\"Make sure the model exists in models.json and the provider has an API key set.\");\r\n _logger.LogWarning(\"\");\r\n _logger.LogWarning(\"Required API keys by model:\");\r\n _logger.LogWarning(\" grok-4.1, x-ai/* -> XAI_API_KEY\");\r\n _logger.LogWarning(\" gpt-4.1*, openai/* -> OPENAI_API_KEY\");\r\n _logger.LogWarning(\" claude-3-*, anthropic/* -> ANTHROPIC_API_KEY\");\r\n _logger.LogWarning(\" openrouter/* -> OPENROUTER_API_KEY\");\r\n _logger.LogWarning(\" deepseek/* -> DEEPSEEK_API_KEY\");\r\n _logger.LogWarning(\" claude-sonnet-4-5 -> ClaudeAgentSDK (no API key needed if Claude Code authenticated)\");\r\n _logger.LogWarning(\"\");\r\n _logger.LogWarning(\"Use '--list-providers' to see provider status.\");\r\n return;\r\n }\r\n\r\n _logger.LogInformation(\"✓ Resolved model: {ModelName}\", resolution.EffectiveModelName);\r\n _logger.LogInformation(\" Provider: {ProviderName}\", resolution.EffectiveProviderName);\r\n _logger.LogInformation(\" Endpoint: {Endpoint}\\n\", resolution.Connection.EndpointUrl);\r\n\r\n // ===== Step 3: Define Tools using FunctionRegistry =====\r\n var registry = new FunctionRegistry();\r\n\r\n // get_weather\r\n _ = registry.AddFunction(\r\n new FunctionContract\r\n {\r\n Name = \"get_weather\",\r\n Description = \"Get the current weather for a location\",\r\n Parameters =\r\n [\r\n new FunctionParameterContract\r\n {\r\n Name = \"location\",\r\n Description = \"The city name (e.g., 'San Francisco', 'New York')\",\r\n ParameterType = SchemaHelper.CreateJsonSchemaFromType(typeof(string)),\r\n IsRequired = true,\r\n },\r\n new FunctionParameterContract\r\n {\r\n Name = \"unit\",\r\n Description = \"Temperature unit: 'celsius' or 'fahrenheit'\",\r\n ParameterType = SchemaHelper.CreateJsonSchemaFromType(typeof(string)),\r\n IsRequired = false,\r\n },\r\n ],\r\n },\r\n async (args) =>\r\n {\r\n var weatherArgs = JsonSerializer.Deserialize<WeatherArgs>(args);\r\n _logger.LogInformation(\"[TOOL] Getting weather for {Location}\", weatherArgs?.Location);\r\n\r\n await Task.Delay(100); // Simulate API call\r\n\r\n var weather = new\r\n {\r\n location = weatherArgs?.Location,\r\n temperature = Random.Shared.Next(60, 85),\r\n unit = weatherArgs?.Unit ?? \"fahrenheit\",\r\n condition = stringArray[Random.Shared.Next(4)],\r\n };\r\n\r\n return JsonSerializer.Serialize(weather);\r\n },\r\n providerName: \"Example\"\r\n );\r\n\r\n // get_time\r\n _ = registry.AddFunction(\r\n new FunctionContract\r\n {\r\n Name = \"get_time\",\r\n Description = \"Get the current time for a timezone\",\r\n Parameters =\r\n [\r\n new FunctionParameterContract\r\n {\r\n Name = \"timezone\",\r\n Description = \"Timezone (e.g., 'PST', 'EST', 'UTC')\",\r\n ParameterType = SchemaHelper.CreateJsonSchemaFromType(typeof(string)),\r\n IsRequired = true,\r\n },\r\n ],\r\n },\r\n async (args) =>\r\n {\r\n var timeArgs = JsonSerializer.Deserialize<TimeArgs>(args);\r\n _logger.LogInformation(\"[TOOL] Getting time for {Timezone}\", timeArgs?.Timezone);\r\n\r\n await Task.Delay(100); // Simulate API call\r\n\r\n var time = new\r\n {\r\n timezone = timeArgs?.Timezone,\r\n time = DateTime.UtcNow.ToString(\"HH:mm:ss\"),\r\n date = DateTime.UtcNow.ToString(\"yyyy-MM-dd\"),\r\n };\r\n\r\n return JsonSerializer.Serialize(time);\r\n },\r\n providerName: \"Example\"\r\n );\r\n\r\n // calculate\r\n _ = registry.AddFunction(\r\n new FunctionContract\r\n {\r\n Name = \"calculate\",\r\n Description = \"Perform a mathematical calculation\",\r\n Parameters =\r\n [\r\n new FunctionParameterContract\r\n {\r\n Name = \"expression\",\r\n Description = \"Mathematical expression (e.g., '2 + 2', '10 * 5')\",\r\n ParameterType = SchemaHelper.CreateJsonSchemaFromType(typeof(string)),\r\n IsRequired = true,\r\n },\r\n ],\r\n },\r\n async (args) =>\r\n {\r\n var calcArgs = JsonSerializer.Deserialize<CalculateArgs>(args);\r\n _logger.LogInformation(\"[TOOL] Calculating: {Expression}\", calcArgs?.Expression);\r\n\r\n await Task.Delay(50); // Simulate processing\r\n\r\n // Simple eval-like calculation (for demo purposes only - unsafe in production)\r\n try\r\n {\r\n var result = EvaluateSimpleExpression(calcArgs?.Expression ?? \"0\");\r\n return JsonSerializer.Serialize(new { result, expression = calcArgs?.Expression });\r\n }\r\n catch (Exception ex)\r\n {\r\n return JsonSerializer.Serialize(new { error = ex.Message });\r\n }\r\n },\r\n providerName: \"Example\"\r\n );\r\n\r\n // Build middleware and handlers from registry\r\n var (toolCallMiddleware, functionHandlers, _) = registry.BuildToolCallComponents(name: \"ToolCallInjection\");\r\n\r\n // ===== Step 4: Create Provider Agent =====\r\n var providerAgent = _agentFactory.CreateStreamingAgent(resolution)\r\n .WithMessageTransformation()\r\n .WithMiddleware(new JsonFragmentUpdateMiddleware())\r\n .WithMiddleware(new MessageUpdateJoinerMiddleware(name: \"MessageJoiner\"))\r\n .WithMiddleware(toolCallMiddleware);\r\n\r\n // ===== Step 6: Start Conversation =====\r\n var conversationHistory = new List<IMessage>\r\n {\r\n new TextMessage { Text = prompt, Role = Role.User },\r\n };\r\n\r\n _logger.LogInformation(\"User: {Prompt}\\n\", prompt);\r\n\r\n // ===== Step 7: Agentic Loop =====\r\n var turnCount = 0;\r\n\r\n while (turnCount < maxTurns)\r\n {\r\n turnCount++;\r\n _logger.LogInformation(\"=== Turn {Turn} ===\", turnCount);\r\n\r\n // Call LLM through middleware chain (streaming)\r\n var streamTask = await providerAgent.GenerateReplyStreamingAsync(\r\n conversationHistory,\r\n new GenerateReplyOptions\r\n {\r\n ModelId = modelId,\r\n // Temperature = temperature,\r\n RunId = Guid.NewGuid().ToString(),\r\n ParentRunId = null,\r\n ThreadId = Guid.NewGuid().ToString(),\r\n }\r\n );\r\n\r\n // Collect messages from stream and execute tool calls in parallel as they arrive\r\n var messages = new List<IMessage>();\r\n var textContent = new System.Text.StringBuilder();\r\n var pendingToolCalls = new Dictionary<string, Task<ToolCallResultMessage>>();\r\n\r\n await foreach (var message in streamTask)\r\n {\r\n messages.Add(message);\r\n\r\n // Display streaming content in real-time and handle tool calls\r\n switch (message)\r\n {\r\n case ReasoningMessage reasoningMsg:\r\n if (!string.IsNullOrEmpty(reasoningMsg.Reasoning))\r\n {\r\n Console.Write(reasoningMsg.Reasoning);\r\n _ = textContent.Append(reasoningMsg.Reasoning);\r\n }\r\n break;\r\n\r\n case TextMessage textMsg:\r\n if (!string.IsNullOrEmpty(textMsg.Text))\r\n {\r\n Console.Write(textMsg.Text);\r\n _ = textContent.Append(textMsg.Text);\r\n }\r\n break;\r\n\r\n case ToolCallMessage toolCall:\r\n // Start executing tool call immediately when we receive it\r\n _logger.LogInformation(\r\n \"[Tool Call] Starting execution: {FunctionName} (id: {ToolCallId})\",\r\n toolCall.FunctionName,\r\n toolCall.ToolCallId\r\n );\r\n\r\n var executionTask = ExecuteToolCallAsync(toolCall, functionHandlers);\r\n pendingToolCalls[toolCall.ToolCallId ?? $\"call_{pendingToolCalls.Count}\"] = executionTask;\r\n break;\r\n\r\n case UsageMessage usage:\r\n _logger.LogDebug(\r\n \"[Usage] Prompt: {Prompt}, Completion: {Completion}\",\r\n usage.Usage.PromptTokens,\r\n usage.Usage.CompletionTokens\r\n );\r\n break;\r\n\r\n default:\r\n // Ignore other message types\r\n break;\r\n }\r\n }\r\n\r\n Console.WriteLine(); // New line after streaming\r\n\r\n // Add all messages to conversation history\r\n conversationHistory.AddRange(messages);\r\n\r\n _logger.LogInformation(\"Received {Count} message(s)\", messages.Count);\r\n\r\n // ===== Step 8: Check for Tool Calls and Await Results =====\r\n if (pendingToolCalls.Count == 0)\r\n {\r\n // No tool calls, conversation complete\r\n _logger.LogInformation(\"✓ No tool calls - conversation complete\\n\");\r\n\r\n if (textContent.Length > 0)\r\n {\r\n _logger.LogInformation(\"Assistant: {Response}\", textContent.ToString());\r\n }\r\n\r\n break;\r\n }\r\n\r\n // ===== Step 9: Await Tool Results and Add to Conversation =====\r\n _logger.LogInformation(\"Awaiting {Count} tool call result(s)...\", pendingToolCalls.Count);\r\n\r\n try\r\n {\r\n // Await all pending tool executions\r\n _ = await Task.WhenAll(pendingToolCalls.Values);\r\n\r\n // Collect results and add to conversation\r\n foreach (var kvp in pendingToolCalls)\r\n {\r\n var result = await kvp.Value;\r\n var resultPreview = result.Result.Length > 100 ? result.Result[..100] + \"...\" : result.Result;\r\n _logger.LogInformation(\" Result for {ToolCallId}: {Result}\", kvp.Key, resultPreview);\r\n\r\n // Add each tool result to conversation history\r\n conversationHistory.Add(result);\r\n }\r\n\r\n _logger.LogInformation(\"Tool execution completed.\");\r\n Console.WriteLine(); // Spacing\r\n }\r\n catch (Exception ex)\r\n {\r\n _logger.LogError(ex, \"Tool execution failed\");\r\n\r\n // Add error message to conversation\r\n var errorMessage = new TextMessage { Text = $\"Error executing tools: {ex.Message}\", Role = Role.User };\r\n conversationHistory.Add(errorMessage);\r\n }\r\n }\r\n\r\n if (turnCount >= maxTurns)\r\n {\r\n _logger.LogWarning(\"⚠ Max turns reached - stopping conversation\");\r\n }\r\n\r\n _logger.LogInformation(\"\\n=== Agentic Loop Complete ===\");\r\n }\r\n\r\n // ===== Helper Methods =====\r\n\r\n /// <summary>\r\n /// Executes a single tool call and returns the result message.\r\n /// </summary>\r\n private async Task<ToolCallResultMessage> ExecuteToolCallAsync(\r\n ToolCallMessage toolCall,\r\n IDictionary<string, Func<string, Task<string>>> handlers\r\n )\r\n {\r\n var toolCallId = toolCall.ToolCallId ?? $\"call_{toolCall.Index}\";\r\n var functionName = toolCall.FunctionName ?? \"unknown\";\r\n var functionArgs = toolCall.FunctionArgs ?? \"{}\";\r\n\r\n try\r\n {\r\n if (handlers.TryGetValue(functionName, out var handler))\r\n {\r\n var result = await handler(functionArgs);\r\n return new ToolCallResultMessage\r\n {\r\n ToolCallId = toolCallId,\r\n Result = result,\r\n Role = Role.User,\r\n FromAgent = toolCall.FromAgent,\r\n GenerationId = toolCall.GenerationId,\r\n };\r\n }\r\n else\r\n {\r\n _logger.LogWarning(\"No handler found for function: {FunctionName}\", functionName);\r\n return new ToolCallResultMessage\r\n {\r\n ToolCallId = toolCallId,\r\n Result = JsonSerializer.Serialize(new { error = $\"Unknown function: {functionName}\" }),\r\n Role = Role.User,\r\n FromAgent = toolCall.FromAgent,\r\n GenerationId = toolCall.GenerationId,\r\n };\r\n }\r\n }\r\n catch (Exception ex)\r\n {\r\n _logger.LogError(ex, \"Error executing tool call: {FunctionName}\", functionName);\r\n return new ToolCallResultMessage\r\n {\r\n ToolCallId = toolCallId,\r\n Result = JsonSerializer.Serialize(new { error = ex.Message }),\r\n Role = Role.User,\r\n FromAgent = toolCall.FromAgent,\r\n GenerationId = toolCall.GenerationId,\r\n };\r\n }\r\n }\r\n\r\n // ===== Helper Classes =====\r\n\r\n private class WeatherArgs\r\n {\r\n public string? Location { get; set; }\r\n public string? Unit { get; set; }\r\n }\r\n\r\n private class TimeArgs\r\n {\r\n public string? Timezone { get; set; }\r\n }\r\n\r\n private class CalculateArgs\r\n {\r\n public string? Expression { get; set; }\r\n }\r\n\r\n // Simple expression evaluator (for demo purposes only - unsafe in production)\r\n private static double EvaluateSimpleExpression(string expression)\r\n {\r\n expression = expression.Replace(\" \", \"\");\r\n\r\n // Very basic calculator - only handles +, -, *, /\r\n if (expression.Contains('+'))\r\n {\r\n var parts = expression.Split('+');\r\n return double.Parse(parts[0]) + double.Parse(parts[1]);\r\n }\r\n if (expression.Contains('-'))\r\n {\r\n var parts = expression.Split('-');\r\n return double.Parse(parts[0]) - double.Parse(parts[1]);\r\n }\r\n if (expression.Contains('*'))\r\n {\r\n var parts = expression.Split('*');\r\n return double.Parse(parts[0]) * double.Parse(parts[1]);\r\n }\r\n if (expression.Contains('/'))\r\n {\r\n var parts = expression.Split('/');\r\n return double.Parse(parts[0]) / double.Parse(parts[1]);\r\n }\r\n\r\n return double.Parse(expression);\r\n }\r\n}\r\n"}, "f6fbc09259cc79ab57c0060c74e0e8c3dfb6a1cc": {"t": 1785756737.8071604, "n": 17932, "h": "d73298ad311ea5a9", "c": "using System.Text.Json;\r\nusing AchieveAi.LmDotnetTools.LmConfig.Agents;\r\nusing AchieveAi.LmDotnetTools.LmCore.Core;\r\nusing AchieveAi.LmDotnetTools.LmCore.Messages;\r\nusing AchieveAi.LmDotnetTools.LmCore.Middleware;\r\nusing AchieveAi.LmDotnetTools.LmCore.Utils;\r\nusing Microsoft.Extensions.Logging;\r\n\r\nnamespace LmConfigUsageExample;\r\n\r\n/// <summary>\r\n/// Example demonstrating an agentic loop using OpenAI provider connected to Grok4.1\r\n/// with the new middleware chain architecture.\r\n///\r\n/// Middleware Chain:\r\n/// Provider Agent => MessageTransformationMiddleware => JsonFragmentUpdateMiddleware\r\n/// => MessageUpdateJoinerMiddleware => ToolCallInjectionMiddleware\r\n///\r\n/// Agentic Loop:\r\n/// 1. Call middleware chain (streaming)\r\n/// 2. Collect all messages from the stream\r\n/// 3. Find ToolsCallMessage in the messages\r\n/// 4. If found, use ToolCallExecutor to execute tool calls asynchronously\r\n/// 5. Add ToolsCallResultMessage to conversation\r\n/// 6. Loop again\r\n/// 7. If no ToolsCallMessage, end the loop\r\n/// </summary>\r\npublic class OpenAiGrokAgenticExample\r\n{\r\n private readonly IModelResolver _modelResolver;\r\n private readonly IProviderAgentFactory _agentFactory;\r\n private readonly ILogger<OpenAiGrokAgenticExample> _logger;\r\n private static readonly string[] stringArray = [\"sunny\", \"cloudy\", \"rainy\", \"partly cloudy\"];\r\n\r\n public OpenAiGrokAgenticExample(\r\n IModelResolver modelResolver,\r\n IProviderAgentFactory agentFactory,\r\n ILogger<OpenAiGrokAgenticExample> logger\r\n )\r\n {\r\n _modelResolver = modelResolver;\r\n _agentFactory = agentFactory;\r\n _logger = logger;\r\n }\r\n\r\n public async Task RunAsync(string prompt, string modelId = \"x-ai/grok-4.1-fast\", float temperature = 0.7f, int maxTurns = 10)\r\n {\r\n _logger.LogInformation(\"=== Agentic Loop Example with {ModelId} ===\\n\", modelId);\r\n\r\n // ===== Step 1: Resolve the specified model =====\r\n var resolution = await _modelResolver.ResolveProviderAsync(modelId);\r\n if (resolution == null)\r\n {\r\n _logger.LogError(\"Failed to resolve model: {ModelId}\", modelId);\r\n _logger.LogWarning(\"Make sure the model exists in models.json and the provider has an API key set.\");\r\n _logger.LogWarning(\"\");\r\n _logger.LogWarning(\"Required API keys by model:\");\r\n _logger.LogWarning(\" grok-4.1, x-ai/* -> XAI_API_KEY\");\r\n _logger.LogWarning(\" gpt-4.1*, openai/* -> OPENAI_API_KEY\");\r\n _logger.LogWarning(\" claude-3-*, anthropic/* -> ANTHROPIC_API_KEY\");\r\n _logger.LogWarning(\" openrouter/* -> OPENROUTER_API_KEY\");\r\n _logger.LogWarning(\" deepseek/* -> DEEPSEEK_API_KEY\");\r\n _logger.LogWarning(\" claude-sonnet-4-5 -> ClaudeAgentSDK (no API key needed if Claude Code authenticated)\");\r\n _logger.LogWarning(\"\");\r\n _logger.LogWarning(\"Use '--list-providers' to see provider status.\");\r\n return;\r\n }\r\n\r\n _logger.LogInformation(\"✓ Resolved model: {ModelName}\", resolution.EffectiveModelName);\r\n _logger.LogInformation(\" Provider: {ProviderName}\", resolution.EffectiveProviderName);\r\n _logger.LogInformation(\" Endpoint: {Endpoint}\\n\", resolution.Connection.EndpointUrl);\r\n\r\n // ===== Step 3: Define Tools using FunctionRegistry =====\r\n var registry = new FunctionRegistry();\r\n\r\n // get_weather\r\n _ = registry.AddFunction(\r\n new FunctionContract\r\n {\r\n Name = \"get_weather\",\r\n Description = \"Get the current weather for a location\",\r\n Parameters =\r\n [\r\n new FunctionParameterContract\r\n {\r\n Name = \"location\",\r\n Description = \"The city name (e.g., 'San Francisco', 'New York')\",\r\n ParameterType = SchemaHelper.CreateJsonSchemaFromType(typeof(string)),\r\n IsRequired = true,\r\n },\r\n new FunctionParameterContract\r\n {\r\n Name = \"unit\",\r\n Description = \"Temperature unit: 'celsius' or 'fahrenheit'\",\r\n ParameterType = SchemaHelper.CreateJsonSchemaFromType(typeof(string)),\r\n IsRequired = false,\r\n },\r\n ],\r\n },\r\n async (args) =>\r\n {\r\n var weatherArgs = JsonSerializer.Deserialize<WeatherArgs>(args);\r\n _logger.LogInformation(\"[TOOL] Getting weather for {Location}\", weatherArgs?.Location);\r\n\r\n await Task.Delay(100); // Simulate API call\r\n\r\n var weather = new\r\n {\r\n location = weatherArgs?.Location,\r\n temperature = Random.Shared.Next(60, 85),\r\n unit = weatherArgs?.Unit ?? \"fahrenheit\",\r\n condition = stringArray[Random.Shared.Next(4)],\r\n };\r\n\r\n return JsonSerializer.Serialize(weather);\r\n },\r\n providerName: \"Example\"\r\n );\r\n\r\n // get_time\r\n _ = registry.AddFunction(\r\n new FunctionContract\r\n {\r\n Name = \"get_time\",\r\n Description = \"Get the current time for a timezone\",\r\n Parameters =\r\n [\r\n new FunctionParameterContract\r\n {\r\n Name = \"timezone\",\r\n Description = \"Timezone (e.g., 'PST', 'EST', 'UTC')\",\r\n ParameterType = SchemaHelper.CreateJsonSchemaFromType(typeof(string)),\r\n IsRequired = true,\r\n },\r\n ],\r\n },\r\n async (args) =>\r\n {\r\n var timeArgs = JsonSerializer.Deserialize<TimeArgs>(args);\r\n _logger.LogInformation(\"[TOOL] Getting time for {Timezone}\", timeArgs?.Timezone);\r\n\r\n await Task.Delay(100); // Simulate API call\r\n\r\n var time = new\r\n {\r\n timezone = timeArgs?.Timezone,\r\n time = DateTime.UtcNow.ToString(\"HH:mm:ss\"),\r\n date = DateTime.UtcNow.ToString(\"yyyy-MM-dd\"),\r\n };\r\n\r\n return JsonSerializer.Serialize(time);\r\n },\r\n providerName: \"Example\"\r\n );\r\n\r\n // calculate\r\n _ = registry.AddFunction(\r\n new FunctionContract\r\n {\r\n Name = \"calculate\",\r\n Description = \"Perform a mathematical calculation\",\r\n Parameters =\r\n [\r\n new FunctionParameterContract\r\n {\r\n Name = \"expression\",\r\n Description = \"Mathematical expression (e.g., '2 + 2', '10 * 5')\",\r\n ParameterType = SchemaHelper.CreateJsonSchemaFromType(typeof(string)),\r\n IsRequired = true,\r\n },\r\n ],\r\n },\r\n async (args) =>\r\n {\r\n var calcArgs = JsonSerializer.Deserialize<CalculateArgs>(args);\r\n _logger.LogInformation(\"[TOOL] Calculating: {Expression}\", calcArgs?.Expression);\r\n\r\n await Task.Delay(50); // Simulate processing\r\n\r\n // Simple eval-like calculation (for demo purposes only - unsafe in production)\r\n try\r\n {\r\n var result = EvaluateSimpleExpression(calcArgs?.Expression ?? \"0\");\r\n return JsonSerializer.Serialize(new { result, expression = calcArgs?.Expression });\r\n }\r\n catch (Exception ex)\r\n {\r\n return JsonSerializer.Serialize(new { error = ex.Message });\r\n }\r\n },\r\n providerName: \"Example\"\r\n );\r\n\r\n // Build middleware and handlers from registry\r\n var (toolCallMiddleware, functionHandlers) = registry.BuildToolCallComponents(name: \"ToolCallInjection\");\r\n\r\n // ===== Step 4: Create Provider Agent =====\r\n var providerAgent = _agentFactory.CreateStreamingAgent(resolution)\r\n .WithMessageTransformation()\r\n .WithMiddleware(new JsonFragmentUpdateMiddleware())\r\n .WithMiddleware(new MessageUpdateJoinerMiddleware(name: \"MessageJoiner\"))\r\n .WithMiddleware(toolCallMiddleware);\r\n\r\n // ===== Step 6: Start Conversation =====\r\n var conversationHistory = new List<IMessage>\r\n {\r\n new TextMessage { Text = prompt, Role = Role.User },\r\n };\r\n\r\n _logger.LogInformation(\"User: {Prompt}\\n\", prompt);\r\n\r\n // ===== Step 7: Agentic Loop =====\r\n var turnCount = 0;\r\n\r\n while (turnCount < maxTurns)\r\n {\r\n turnCount++;\r\n _logger.LogInformation(\"=== Turn {Turn} ===\", turnCount);\r\n\r\n // Call LLM through middleware chain (streaming)\r\n var streamTask = await providerAgent.GenerateReplyStreamingAsync(\r\n conversationHistory,\r\n new GenerateReplyOptions\r\n {\r\n ModelId = modelId,\r\n // Temperature = temperature,\r\n RunId = Guid.NewGuid().ToString(),\r\n ParentRunId = null,\r\n ThreadId = Guid.NewGuid().ToString(),\r\n }\r\n );\r\n\r\n // Collect messages from stream and execute tool calls in parallel as they arrive\r\n var messages = new List<IMessage>();\r\n var textContent = new System.Text.StringBuilder();\r\n var pendingToolCalls = new Dictionary<string, Task<ToolCallResultMessage>>();\r\n\r\n await foreach (var message in streamTask)\r\n {\r\n messages.Add(message);\r\n\r\n // Display streaming content in real-time and handle tool calls\r\n switch (message)\r\n {\r\n case ReasoningMessage reasoningMsg:\r\n if (!string.IsNullOrEmpty(reasoningMsg.Reasoning))\r\n {\r\n Console.Write(reasoningMsg.Reasoning);\r\n _ = textContent.Append(reasoningMsg.Reasoning);\r\n }\r\n break;\r\n\r\n case TextMessage textMsg:\r\n if (!string.IsNullOrEmpty(textMsg.Text))\r\n {\r\n Console.Write(textMsg.Text);\r\n _ = textContent.Append(textMsg.Text);\r\n }\r\n break;\r\n\r\n case ToolCallMessage toolCall:\r\n // Start executing tool call immediately when we receive it\r\n _logger.LogInformation(\r\n \"[Tool Call] Starting execution: {FunctionName} (id: {ToolCallId})\",\r\n toolCall.FunctionName,\r\n toolCall.ToolCallId\r\n );\r\n\r\n var executionTask = ExecuteToolCallAsync(toolCall, functionHandlers);\r\n pendingToolCalls[toolCall.ToolCallId ?? $\"call_{pendingToolCalls.Count}\"] = executionTask;\r\n break;\r\n\r\n case UsageMessage usage:\r\n _logger.LogDebug(\r\n \"[Usage] Prompt: {Prompt}, Completion: {Completion}\",\r\n usage.Usage.PromptTokens,\r\n usage.Usage.CompletionTokens\r\n );\r\n break;\r\n\r\n default:\r\n // Ignore other message types\r\n break;\r\n }\r\n }\r\n\r\n Console.WriteLine(); // New line after streaming\r\n\r\n // Add all messages to conversation history\r\n conversationHistory.AddRange(messages);\r\n\r\n _logger.LogInformation(\"Received {Count} message(s)\", messages.Count);\r\n\r\n // ===== Step 8: Check for Tool Calls and Await Results =====\r\n if (pendingToolCalls.Count == 0)\r\n {\r\n // No tool calls, conversation complete\r\n _logger.LogInformation(\"✓ No tool calls - conversation complete\\n\");\r\n\r\n if (textContent.Length > 0)\r\n {\r\n _logger.LogInformation(\"Assistant: {Response}\", textContent.ToString());\r\n }\r\n\r\n break;\r\n }\r\n\r\n // ===== Step 9: Await Tool Results and Add to Conversation =====\r\n _logger.LogInformation(\"Awaiting {Count} tool call result(s)...\", pendingToolCalls.Count);\r\n\r\n try\r\n {\r\n // Await all pending tool executions\r\n _ = await Task.WhenAll(pendingToolCalls.Values);\r\n\r\n // Collect results and add to conversation\r\n foreach (var kvp in pendingToolCalls)\r\n {\r\n var result = await kvp.Value;\r\n var resultPreview = result.Result.Length > 100 ? result.Result[..100] + \"...\" : result.Result;\r\n _logger.LogInformation(\" Result for {ToolCallId}: {Result}\", kvp.Key, resultPreview);\r\n\r\n // Add each tool result to conversation history\r\n conversationHistory.Add(result);\r\n }\r\n\r\n _logger.LogInformation(\"Tool execution completed.\");\r\n Console.WriteLine(); // Spacing\r\n }\r\n catch (Exception ex)\r\n {\r\n _logger.LogError(ex, \"Tool execution failed\");\r\n\r\n // Add error message to conversation\r\n var errorMessage = new TextMessage { Text = $\"Error executing tools: {ex.Message}\", Role = Role.User };\r\n conversationHistory.Add(errorMessage);\r\n }\r\n }\r\n\r\n if (turnCount >= maxTurns)\r\n {\r\n _logger.LogWarning(\"⚠ Max turns reached - stopping conversation\");\r\n }\r\n\r\n _logger.LogInformation(\"\\n=== Agentic Loop Complete ===\");\r\n }\r\n\r\n // ===== Helper Methods =====\r\n\r\n /// <summary>\r\n /// Executes a single tool call and returns the result message.\r\n /// </summary>\r\n private async Task<ToolCallResultMessage> ExecuteToolCallAsync(\r\n ToolCallMessage toolCall,\r\n IDictionary<string, Func<string, Task<string>>> handlers\r\n )\r\n {\r\n var toolCallId = toolCall.ToolCallId ?? $\"call_{toolCall.Index}\";\r\n var functionName = toolCall.FunctionName ?? \"unknown\";\r\n var functionArgs = toolCall.FunctionArgs ?? \"{}\";\r\n\r\n try\r\n {\r\n if (handlers.TryGetValue(functionName, out var handler))\r\n {\r\n var result = await handler(functionArgs);\r\n return new ToolCallResultMessage\r\n {\r\n ToolCallId = toolCallId,\r\n Result = result,\r\n Role = Role.User,\r\n FromAgent = toolCall.FromAgent,\r\n GenerationId = toolCall.GenerationId,\r\n };\r\n }\r\n else\r\n {\r\n _logger.LogWarning(\"No handler found for function: {FunctionName}\", functionName);\r\n return new ToolCallResultMessage\r\n {\r\n ToolCallId = toolCallId,\r\n Result = JsonSerializer.Serialize(new { error = $\"Unknown function: {functionName}\" }),\r\n Role = Role.User,\r\n FromAgent = toolCall.FromAgent,\r\n GenerationId = toolCall.GenerationId,\r\n };\r\n }\r\n }\r\n catch (Exception ex)\r\n {\r\n _logger.LogError(ex, \"Error executing tool call: {FunctionName}\", functionName);\r\n return new ToolCallResultMessage\r\n {\r\n ToolCallId = toolCallId,\r\n Result = JsonSerializer.Serialize(new { error = ex.Message }),\r\n Role = Role.User,\r\n FromAgent = toolCall.FromAgent,\r\n GenerationId = toolCall.GenerationId,\r\n };\r\n }\r\n }\r\n\r\n // ===== Helper Classes =====\r\n\r\n private class WeatherArgs\r\n {\r\n public string? Location { get; set; }\r\n public string? Unit { get; set; }\r\n }\r\n\r\n private class TimeArgs\r\n {\r\n public string? Timezone { get; set; }\r\n }\r\n\r\n private class CalculateArgs\r\n {\r\n public string? Expression { get; set; }\r\n }\r\n\r\n // Simple expression evaluator (for demo purposes only - unsafe in production)\r\n private static double EvaluateSimpleExpression(string expression)\r\n {\r\n expression = expression.Replace(\" \", \"\");\r\n\r\n // Very basic calculator - only handles +, -, *, /\r\n if (expression.Contains('+'))\r\n {\r\n var parts = expression.Split('+');\r\n return double.Parse(parts[0]) + double.Parse(parts[1]);\r\n }\r\n if (expression.Contains('-'))\r\n {\r\n var parts = expression.Split('-');\r\n return double.Parse(parts[0]) - double.Parse(parts[1]);\r\n }\r\n if (expression.Contains('*'))\r\n {\r\n var parts = expression.Split('*');\r\n return double.Parse(parts[0]) * double.Parse(parts[1]);\r\n }\r\n if (expression.Contains('/'))\r\n {\r\n var parts = expression.Split('/');\r\n return double.Parse(parts[0]) / double.Parse(parts[1]);\r\n }\r\n\r\n return double.Parse(expression);\r\n }\r\n}\r\n"}, "03bb4344e55549129a4e557c4ee5443fba7e6a90": {"t": 1785756739.5514913, "n": 17482, "h": "97402165d2b8d8fe", "c": "using System.Text.Json;\nusing AchieveAi.LmDotnetTools.LmConfig.Agents;\nusing AchieveAi.LmDotnetTools.LmCore.Core;\nusing AchieveAi.LmDotnetTools.LmCore.Messages;\nusing AchieveAi.LmDotnetTools.LmCore.Middleware;\nusing AchieveAi.LmDotnetTools.LmCore.Utils;\nusing Microsoft.Extensions.Logging;\n\nnamespace LmConfigUsageExample;\n\n/// <summary>\n/// Example demonstrating an agentic loop using OpenAI provider connected to Grok4.1\n/// with the new middleware chain architecture.\n///\n/// Middleware Chain:\n/// Provider Agent => MessageTransformationMiddleware => JsonFragmentUpdateMiddleware\n/// => MessageUpdateJoinerMiddleware => ToolCallInjectionMiddleware\n///\n/// Agentic Loop:\n/// 1. Call middleware chain (streaming)\n/// 2. Collect all messages from the stream\n/// 3. Find ToolsCallMessage in the messages\n/// 4. If found, use ToolCallExecutor to execute tool calls asynchronously\n/// 5. Add ToolsCallResultMessage to conversation\n/// 6. Loop again\n/// 7. If no ToolsCallMessage, end the loop\n/// </summary>\npublic class OpenAiGrokAgenticExample\n{\n private readonly IModelResolver _modelResolver;\n private readonly IProviderAgentFactory _agentFactory;\n private readonly ILogger<OpenAiGrokAgenticExample> _logger;\n private static readonly string[] stringArray = [\"sunny\", \"cloudy\", \"rainy\", \"partly cloudy\"];\n\n public OpenAiGrokAgenticExample(\n IModelResolver modelResolver,\n IProviderAgentFactory agentFactory,\n ILogger<OpenAiGrokAgenticExample> logger\n )\n {\n _modelResolver = modelResolver;\n _agentFactory = agentFactory;\n _logger = logger;\n }\n\n public async Task RunAsync(string prompt, string modelId = \"x-ai/grok-4.1-fast\", float temperature = 0.7f, int maxTurns = 10)\n {\n _logger.LogInformation(\"=== Agentic Loop Example with {ModelId} ===\\n\", modelId);\n\n // ===== Step 1: Resolve the specified model =====\n var resolution = await _modelResolver.ResolveProviderAsync(modelId);\n if (resolution == null)\n {\n _logger.LogError(\"Failed to resolve model: {ModelId}\", modelId);\n _logger.LogWarning(\"Make sure the model exists in models.json and the provider has an API key set.\");\n _logger.LogWarning(\"\");\n _logger.LogWarning(\"Required API keys by model:\");\n _logger.LogWarning(\" grok-4.1, x-ai/* -> XAI_API_KEY\");\n _logger.LogWarning(\" gpt-4.1*, openai/* -> OPENAI_API_KEY\");\n _logger.LogWarning(\" claude-3-*, anthropic/* -> ANTHROPIC_API_KEY\");\n _logger.LogWarning(\" openrouter/* -> OPENROUTER_API_KEY\");\n _logger.LogWarning(\" deepseek/* -> DEEPSEEK_API_KEY\");\n _logger.LogWarning(\" claude-sonnet-4-5 -> ClaudeAgentSDK (no API key needed if Claude Code authenticated)\");\n _logger.LogWarning(\"\");\n _logger.LogWarning(\"Use '--list-providers' to see provider status.\");\n return;\n }\n\n _logger.LogInformation(\"✓ Resolved model: {ModelName}\", resolution.EffectiveModelName);\n _logger.LogInformation(\" Provider: {ProviderName}\", resolution.EffectiveProviderName);\n _logger.LogInformation(\" Endpoint: {Endpoint}\\n\", resolution.Connection.EndpointUrl);\n\n // ===== Step 3: Define Tools using FunctionRegistry =====\n var registry = new FunctionRegistry();\n\n // get_weather\n _ = registry.AddFunction(\n new FunctionContract\n {\n Name = \"get_weather\",\n Description = \"Get the current weather for a location\",\n Parameters =\n [\n new FunctionParameterContract\n {\n Name = \"location\",\n Description = \"The city name (e.g., 'San Francisco', 'New York')\",\n ParameterType = SchemaHelper.CreateJsonSchemaFromType(typeof(string)),\n IsRequired = true,\n },\n new FunctionParameterContract\n {\n Name = \"unit\",\n Description = \"Temperature unit: 'celsius' or 'fahrenheit'\",\n ParameterType = SchemaHelper.CreateJsonSchemaFromType(typeof(string)),\n IsRequired = false,\n },\n ],\n },\n async (args) =>\n {\n var weatherArgs = JsonSerializer.Deserialize<WeatherArgs>(args);\n _logger.LogInformation(\"[TOOL] Getting weather for {Location}\", weatherArgs?.Location);\n\n await Task.Delay(100); // Simulate API call\n\n var weather = new\n {\n location = weatherArgs?.Location,\n temperature = Random.Shared.Next(60, 85),\n unit = weatherArgs?.Unit ?? \"fahrenheit\",\n condition = stringArray[Random.Shared.Next(4)],\n };\n\n return JsonSerializer.Serialize(weather);\n },\n providerName: \"Example\"\n );\n\n // get_time\n _ = registry.AddFunction(\n new FunctionContract\n {\n Name = \"get_time\",\n Description = \"Get the current time for a timezone\",\n Parameters =\n [\n new FunctionParameterContract\n {\n Name = \"timezone\",\n Description = \"Timezone (e.g., 'PST', 'EST', 'UTC')\",\n ParameterType = SchemaHelper.CreateJsonSchemaFromType(typeof(string)),\n IsRequired = true,\n },\n ],\n },\n async (args) =>\n {\n var timeArgs = JsonSerializer.Deserialize<TimeArgs>(args);\n _logger.LogInformation(\"[TOOL] Getting time for {Timezone}\", timeArgs?.Timezone);\n\n await Task.Delay(100); // Simulate API call\n\n var time = new\n {\n timezone = timeArgs?.Timezone,\n time = DateTime.UtcNow.ToString(\"HH:mm:ss\"),\n date = DateTime.UtcNow.ToString(\"yyyy-MM-dd\"),\n };\n\n return JsonSerializer.Serialize(time);\n },\n providerName: \"Example\"\n );\n\n // calculate\n _ = registry.AddFunction(\n new FunctionContract\n {\n Name = \"calculate\",\n Description = \"Perform a mathematical calculation\",\n Parameters =\n [\n new FunctionParameterContract\n {\n Name = \"expression\",\n Description = \"Mathematical expression (e.g., '2 + 2', '10 * 5')\",\n ParameterType = SchemaHelper.CreateJsonSchemaFromType(typeof(string)),\n IsRequired = true,\n },\n ],\n },\n async (args) =>\n {\n var calcArgs = JsonSerializer.Deserialize<CalculateArgs>(args);\n _logger.LogInformation(\"[TOOL] Calculating: {Expression}\", calcArgs?.Expression);\n\n await Task.Delay(50); // Simulate processing\n\n // Simple eval-like calculation (for demo purposes only - unsafe in production)\n try\n {\n var result = EvaluateSimpleExpression(calcArgs?.Expression ?? \"0\");\n return JsonSerializer.Serialize(new { result, expression = calcArgs?.Expression });\n }\n catch (Exception ex)\n {\n return JsonSerializer.Serialize(new { error = ex.Message });\n }\n },\n providerName: \"Example\"\n );\n\n // Build middleware and handlers from registry\n var (toolCallMiddleware, functionHandlers) = registry.BuildToolCallComponents(name: \"ToolCallInjection\");\n\n // ===== Step 4: Create Provider Agent =====\n var providerAgent = _agentFactory.CreateStreamingAgent(resolution)\n .WithMessageTransformation()\n .WithMiddleware(new JsonFragmentUpdateMiddleware())\n .WithMiddleware(new MessageUpdateJoinerMiddleware(name: \"MessageJoiner\"))\n .WithMiddleware(toolCallMiddleware);\n\n // ===== Step 6: Start Conversation =====\n var conversationHistory = new List<IMessage>\n {\n new TextMessage { Text = prompt, Role = Role.User },\n };\n\n _logger.LogInformation(\"User: {Prompt}\\n\", prompt);\n\n // ===== Step 7: Agentic Loop =====\n var turnCount = 0;\n\n while (turnCount < maxTurns)\n {\n turnCount++;\n _logger.LogInformation(\"=== Turn {Turn} ===\", turnCount);\n\n // Call LLM through middleware chain (streaming)\n var streamTask = await providerAgent.GenerateReplyStreamingAsync(\n conversationHistory,\n new GenerateReplyOptions\n {\n ModelId = modelId,\n // Temperature = temperature,\n RunId = Guid.NewGuid().ToString(),\n ParentRunId = null,\n ThreadId = Guid.NewGuid().ToString(),\n }\n );\n\n // Collect messages from stream and execute tool calls in parallel as they arrive\n var messages = new List<IMessage>();\n var textContent = new System.Text.StringBuilder();\n var pendingToolCalls = new Dictionary<string, Task<ToolCallResultMessage>>();\n\n await foreach (var message in streamTask)\n {\n messages.Add(message);\n\n // Display streaming content in real-time and handle tool calls\n switch (message)\n {\n case ReasoningMessage reasoningMsg:\n if (!string.IsNullOrEmpty(reasoningMsg.Reasoning))\n {\n Console.Write(reasoningMsg.Reasoning);\n _ = textContent.Append(reasoningMsg.Reasoning);\n }\n break;\n\n case TextMessage textMsg:\n if (!string.IsNullOrEmpty(textMsg.Text))\n {\n Console.Write(textMsg.Text);\n _ = textContent.Append(textMsg.Text);\n }\n break;\n\n case ToolCallMessage toolCall:\n // Start executing tool call immediately when we receive it\n _logger.LogInformation(\n \"[Tool Call] Starting execution: {FunctionName} (id: {ToolCallId})\",\n toolCall.FunctionName,\n toolCall.ToolCallId\n );\n\n var executionTask = ExecuteToolCallAsync(toolCall, functionHandlers);\n pendingToolCalls[toolCall.ToolCallId ?? $\"call_{pendingToolCalls.Count}\"] = executionTask;\n break;\n\n case UsageMessage usage:\n _logger.LogDebug(\n \"[Usage] Prompt: {Prompt}, Completion: {Completion}\",\n usage.Usage.PromptTokens,\n usage.Usage.CompletionTokens\n );\n break;\n\n default:\n // Ignore other message types\n break;\n }\n }\n\n Console.WriteLine(); // New line after streaming\n\n // Add all messages to conversation history\n conversationHistory.AddRange(messages);\n\n _logger.LogInformation(\"Received {Count} message(s)\", messages.Count);\n\n // ===== Step 8: Check for Tool Calls and Await Results =====\n if (pendingToolCalls.Count == 0)\n {\n // No tool calls, conversation complete\n _logger.LogInformation(\"✓ No tool calls - conversation complete\\n\");\n\n if (textContent.Length > 0)\n {\n _logger.LogInformation(\"Assistant: {Response}\", textContent.ToString());\n }\n\n break;\n }\n\n // ===== Step 9: Await Tool Results and Add to Conversation =====\n _logger.LogInformation(\"Awaiting {Count} tool call result(s)...\", pendingToolCalls.Count);\n\n try\n {\n // Await all pending tool executions\n _ = await Task.WhenAll(pendingToolCalls.Values);\n\n // Collect results and add to conversation\n foreach (var kvp in pendingToolCalls)\n {\n var result = await kvp.Value;\n var resultPreview = result.Result.Length > 100 ? result.Result[..100] + \"...\" : result.Result;\n _logger.LogInformation(\" Result for {ToolCallId}: {Result}\", kvp.Key, resultPreview);\n\n // Add each tool result to conversation history\n conversationHistory.Add(result);\n }\n\n _logger.LogInformation(\"Tool execution completed.\");\n Console.WriteLine(); // Spacing\n }\n catch (Exception ex)\n {\n _logger.LogError(ex, \"Tool execution failed\");\n\n // Add error message to conversation\n var errorMessage = new TextMessage { Text = $\"Error executing tools: {ex.Message}\", Role = Role.User };\n conversationHistory.Add(errorMessage);\n }\n }\n\n if (turnCount >= maxTurns)\n {\n _logger.LogWarning(\"⚠ Max turns reached - stopping conversation\");\n }\n\n _logger.LogInformation(\"\\n=== Agentic Loop Complete ===\");\n }\n\n // ===== Helper Methods =====\n\n /// <summary>\n /// Executes a single tool call and returns the result message.\n /// </summary>\n private async Task<ToolCallResultMessage> ExecuteToolCallAsync(\n ToolCallMessage toolCall,\n IDictionary<string, Func<string, Task<string>>> handlers\n )\n {\n var toolCallId = toolCall.ToolCallId ?? $\"call_{toolCall.Index}\";\n var functionName = toolCall.FunctionName ?? \"unknown\";\n var functionArgs = toolCall.FunctionArgs ?? \"{}\";\n\n try\n {\n if (handlers.TryGetValue(functionName, out var handler))\n {\n var result = await handler(functionArgs);\n return new ToolCallResultMessage\n {\n ToolCallId = toolCallId,\n Result = result,\n Role = Role.User,\n FromAgent = toolCall.FromAgent,\n GenerationId = toolCall.GenerationId,\n };\n }\n else\n {\n _logger.LogWarning(\"No handler found for function: {FunctionName}\", functionName);\n return new ToolCallResultMessage\n {\n ToolCallId = toolCallId,\n Result = JsonSerializer.Serialize(new { error = $\"Unknown function: {functionName}\" }),\n Role = Role.User,\n FromAgent = toolCall.FromAgent,\n GenerationId = toolCall.GenerationId,\n };\n }\n }\n catch (Exception ex)\n {\n _logger.LogError(ex, \"Error executing tool call: {FunctionName}\", functionName);\n return new ToolCallResultMessage\n {\n ToolCallId = toolCallId,\n Result = JsonSerializer.Serialize(new { error = ex.Message }),\n Role = Role.User,\n FromAgent = toolCall.FromAgent,\n GenerationId = toolCall.GenerationId,\n };\n }\n }\n\n // ===== Helper Classes =====\n\n private class WeatherArgs\n {\n public string? Location { get; set; }\n public string? Unit { get; set; }\n }\n\n private class TimeArgs\n {\n public string? Timezone { get; set; }\n }\n\n private class CalculateArgs\n {\n public string? Expression { get; set; }\n }\n\n // Simple expression evaluator (for demo purposes only - unsafe in production)\n private static double EvaluateSimpleExpression(string expression)\n {\n expression = expression.Replace(\" \", \"\");\n\n // Very basic calculator - only handles +, -, *, /\n if (expression.Contains('+'))\n {\n var parts = expression.Split('+');\n return double.Parse(parts[0]) + double.Parse(parts[1]);\n }\n if (expression.Contains('-'))\n {\n var parts = expression.Split('-');\n return double.Parse(parts[0]) - double.Parse(parts[1]);\n }\n if (expression.Contains('*'))\n {\n var parts = expression.Split('*');\n return double.Parse(parts[0]) * double.Parse(parts[1]);\n }\n if (expression.Contains('/'))\n {\n var parts = expression.Split('/');\n return double.Parse(parts[0]) / double.Parse(parts[1]);\n }\n\n return double.Parse(expression);\n }\n}\n"}, "256b7b69ce737c03b0d8b71dfdfe48640140eabd": {"t": 1785756741.2986786, "n": 17356, "h": "337d65351e93f18b", "c": "using System.Text.Json;\nusing AchieveAi.LmDotnetTools.LmConfig.Agents;\nusing AchieveAi.LmDotnetTools.LmCore.Agents;\nusing AchieveAi.LmDotnetTools.LmCore.Core;\nusing AchieveAi.LmDotnetTools.LmCore.Messages;\nusing AchieveAi.LmDotnetTools.LmCore.Middleware;\nusing AchieveAi.LmDotnetTools.LmCore.Utils;\nusing Microsoft.Extensions.Logging;\n\nnamespace LmConfigUsageExample;\n\n/// <summary>\n/// Example demonstrating an agentic loop using OpenAI provider connected to Grok4.1\n/// with the new middleware chain architecture.\n///\n/// Middleware Chain:\n/// Provider Agent => MessageTransformationMiddleware => JsonFragmentUpdateMiddleware\n/// => MessageUpdateJoinerMiddleware => ToolCallInjectionMiddleware\n///\n/// Agentic Loop:\n/// 1. Call middleware chain (streaming)\n/// 2. Collect all messages from the stream\n/// 3. Find ToolsCallMessage in the messages\n/// 4. If found, use ToolCallExecutor to execute tool calls asynchronously\n/// 5. Add ToolsCallResultMessage to conversation\n/// 6. Loop again\n/// 7. If no ToolsCallMessage, end the loop\n/// </summary>\npublic class OpenAiGrokAgenticExample\n{\n private readonly IModelResolver _modelResolver;\n private readonly IProviderAgentFactory _agentFactory;\n private readonly ILogger<OpenAiGrokAgenticExample> _logger;\n\n public OpenAiGrokAgenticExample(\n IModelResolver modelResolver,\n IProviderAgentFactory agentFactory,\n ILogger<OpenAiGrokAgenticExample> logger\n )\n {\n _modelResolver = modelResolver;\n _agentFactory = agentFactory;\n _logger = logger;\n }\n\n public async Task RunAsync(string prompt, string modelId = \"x-ai/grok-4.1-fast\", float temperature = 0.7f, int maxTurns = 10)\n {\n _logger.LogInformation(\"=== Agentic Loop Example with {ModelId} ===\\n\", modelId);\n\n // ===== Step 1: Resolve the specified model =====\n var resolution = await _modelResolver.ResolveProviderAsync(modelId);\n if (resolution == null)\n {\n _logger.LogError(\"Failed to resolve model: {ModelId}\", modelId);\n _logger.LogWarning(\"Make sure the model exists in models.json and the provider has an API key set.\");\n _logger.LogWarning(\"\");\n _logger.LogWarning(\"Required API keys by model:\");\n _logger.LogWarning(\" grok-4.1, x-ai/* -> XAI_API_KEY\");\n _logger.LogWarning(\" gpt-4.1*, openai/* -> OPENAI_API_KEY\");\n _logger.LogWarning(\" claude-3-*, anthropic/* -> ANTHROPIC_API_KEY\");\n _logger.LogWarning(\" openrouter/* -> OPENROUTER_API_KEY\");\n _logger.LogWarning(\" deepseek/* -> DEEPSEEK_API_KEY\");\n _logger.LogWarning(\" claude-sonnet-4-5 -> ClaudeAgentSDK (no API key needed if Claude Code authenticated)\");\n _logger.LogWarning(\"\");\n _logger.LogWarning(\"Use '--list-providers' to see provider status.\");\n return;\n }\n\n _logger.LogInformation(\"✓ Resolved model: {ModelName}\", resolution.EffectiveModelName);\n _logger.LogInformation(\" Provider: {ProviderName}\", resolution.EffectiveProviderName);\n _logger.LogInformation(\" Endpoint: {Endpoint}\\n\", resolution.Connection.EndpointUrl);\n\n // ===== Step 3: Define Tools using FunctionRegistry =====\n var registry = new FunctionRegistry();\n\n // get_weather\n _ = registry.AddFunction(\n new FunctionContract\n {\n Name = \"get_weather\",\n Description = \"Get the current weather for a location\",\n Parameters =\n [\n new FunctionParameterContract\n {\n Name = \"location\",\n Description = \"The city name (e.g., 'San Francisco', 'New York')\",\n ParameterType = SchemaHelper.CreateJsonSchemaFromType(typeof(string)),\n IsRequired = true,\n },\n new FunctionParameterContract\n {\n Name = \"unit\",\n Description = \"Temperature unit: 'celsius' or 'fahrenheit'\",\n ParameterType = SchemaHelper.CreateJsonSchemaFromType(typeof(string)),\n IsRequired = false,\n },\n ],\n },\n async (args) =>\n {\n var weatherArgs = JsonSerializer.Deserialize<WeatherArgs>(args);\n _logger.LogInformation(\"[TOOL] Getting weather for {Location}\", weatherArgs?.Location);\n\n await Task.Delay(100); // Simulate API call\n\n var weather = new\n {\n location = weatherArgs?.Location,\n temperature = Random.Shared.Next(60, 85),\n unit = weatherArgs?.Unit ?? \"fahrenheit\",\n condition = new[] { \"sunny\", \"cloudy\", \"rainy\", \"partly cloudy\" }[Random.Shared.Next(4)],\n };\n\n return JsonSerializer.Serialize(weather);\n },\n providerName: \"Example\"\n );\n\n // get_time\n _ = registry.AddFunction(\n new FunctionContract\n {\n Name = \"get_time\",\n Description = \"Get the current time for a timezone\",\n Parameters =\n [\n new FunctionParameterContract\n {\n Name = \"timezone\",\n Description = \"Timezone (e.g., 'PST', 'EST', 'UTC')\",\n ParameterType = SchemaHelper.CreateJsonSchemaFromType(typeof(string)),\n IsRequired = true,\n },\n ],\n },\n async (args) =>\n {\n var timeArgs = JsonSerializer.Deserialize<TimeArgs>(args);\n _logger.LogInformation(\"[TOOL] Getting time for {Timezone}\", timeArgs?.Timezone);\n\n await Task.Delay(100); // Simulate API call\n\n var time = new\n {\n timezone = timeArgs?.Timezone,\n time = DateTime.UtcNow.ToString(\"HH:mm:ss\"),\n date = DateTime.UtcNow.ToString(\"yyyy-MM-dd\"),\n };\n\n return JsonSerializer.Serialize(time);\n },\n providerName: \"Example\"\n );\n\n // calculate\n _ = registry.AddFunction(\n new FunctionContract\n {\n Name = \"calculate\",\n Description = \"Perform a mathematical calculation\",\n Parameters =\n [\n new FunctionParameterContract\n {\n Name = \"expression\",\n Description = \"Mathematical expression (e.g., '2 + 2', '10 * 5')\",\n ParameterType = SchemaHelper.CreateJsonSchemaFromType(typeof(string)),\n IsRequired = true,\n },\n ],\n },\n async (args) =>\n {\n var calcArgs = JsonSerializer.Deserialize<CalculateArgs>(args);\n _logger.LogInformation(\"[TOOL] Calculating: {Expression}\", calcArgs?.Expression);\n\n await Task.Delay(50); // Simulate processing\n\n // Simple eval-like calculation (for demo purposes only - unsafe in production)\n try\n {\n var result = EvaluateSimpleExpression(calcArgs?.Expression ?? \"0\");\n return JsonSerializer.Serialize(new { result, expression = calcArgs?.Expression });\n }\n catch (Exception ex)\n {\n return JsonSerializer.Serialize(new { error = ex.Message });\n }\n },\n providerName: \"Example\"\n );\n\n // Build middleware and handlers from registry\n var (toolCallMiddleware, functionHandlers) = registry.BuildToolCallComponents(name: \"ToolCallInjection\");\n\n // ===== Step 4: Create Provider Agent =====\n var providerAgent = _agentFactory.CreateStreamingAgent(resolution)\n .WithMessageTransformation()\n .WithMiddleware(new JsonFragmentUpdateMiddleware())\n .WithMiddleware(new MessageUpdateJoinerMiddleware(name: \"MessageJoiner\"))\n .WithMiddleware(toolCallMiddleware);\n\n // ===== Step 6: Start Conversation =====\n var conversationHistory = new List<IMessage>\n {\n new TextMessage { Text = prompt, Role = Role.User },\n };\n\n _logger.LogInformation(\"User: {Prompt}\\n\", prompt);\n\n // ===== Step 7: Agentic Loop =====\n var turnCount = 0;\n\n while (turnCount < maxTurns)\n {\n turnCount++;\n _logger.LogInformation(\"=== Turn {Turn} ===\", turnCount);\n\n // Call LLM through middleware chain (streaming)\n var streamTask = await providerAgent.GenerateReplyStreamingAsync(\n conversationHistory,\n new GenerateReplyOptions\n {\n ModelId = modelId,\n // Temperature = temperature,\n RunId = Guid.NewGuid().ToString(),\n ParentRunId = null,\n ThreadId = Guid.NewGuid().ToString(),\n }\n );\n\n // Collect messages from stream and execute tool calls in parallel as they arrive\n var messages = new List<IMessage>();\n var textContent = new System.Text.StringBuilder();\n var pendingToolCalls = new Dictionary<string, Task<ToolCallResultMessage>>();\n\n await foreach (var message in streamTask)\n {\n messages.Add(message);\n\n // Display streaming content in real-time and handle tool calls\n switch (message)\n {\n case ReasoningMessage reasoningMsg:\n if (!string.IsNullOrEmpty(reasoningMsg.Reasoning))\n {\n Console.Write(reasoningMsg.Reasoning);\n _ = textContent.Append(reasoningMsg.Reasoning);\n }\n break;\n\n case TextMessage textMsg:\n if (!string.IsNullOrEmpty(textMsg.Text))\n {\n Console.Write(textMsg.Text);\n _ = textContent.Append(textMsg.Text);\n }\n break;\n\n case ToolCallMessage toolCall:\n // Start executing tool call immediately when we receive it\n _logger.LogInformation(\n \"[Tool Call] Starting execution: {FunctionName} (id: {ToolCallId})\",\n toolCall.FunctionName,\n toolCall.ToolCallId\n );\n\n var executionTask = ExecuteToolCallAsync(toolCall, functionHandlers);\n pendingToolCalls[toolCall.ToolCallId ?? $\"call_{pendingToolCalls.Count}\"] = executionTask;\n break;\n\n case UsageMessage usage:\n _logger.LogDebug(\n \"[Usage] Prompt: {Prompt}, Completion: {Completion}\",\n usage.Usage.PromptTokens,\n usage.Usage.CompletionTokens\n );\n break;\n }\n }\n\n Console.WriteLine(); // New line after streaming\n\n // Add all messages to conversation history\n conversationHistory.AddRange(messages);\n\n _logger.LogInformation(\"Received {Count} message(s)\", messages.Count);\n\n // ===== Step 8: Check for Tool Calls and Await Results =====\n if (pendingToolCalls.Count == 0)\n {\n // No tool calls, conversation complete\n _logger.LogInformation(\"✓ No tool calls - conversation complete\\n\");\n\n if (textContent.Length > 0)\n {\n _logger.LogInformation(\"Assistant: {Response}\", textContent.ToString());\n }\n\n break;\n }\n\n // ===== Step 9: Await Tool Results and Add to Conversation =====\n _logger.LogInformation(\"Awaiting {Count} tool call result(s)...\", pendingToolCalls.Count);\n\n try\n {\n // Await all pending tool executions\n _ = await Task.WhenAll(pendingToolCalls.Values);\n\n // Collect results and add to conversation\n foreach (var kvp in pendingToolCalls)\n {\n var result = await kvp.Value;\n var resultPreview = result.Result.Length > 100 ? result.Result[..100] + \"...\" : result.Result;\n _logger.LogInformation(\" Result for {ToolCallId}: {Result}\", kvp.Key, resultPreview);\n\n // Add each tool result to conversation history\n conversationHistory.Add(result);\n }\n\n _logger.LogInformation(\"Tool execution completed.\");\n Console.WriteLine(); // Spacing\n }\n catch (Exception ex)\n {\n _logger.LogError(ex, \"Tool execution failed\");\n\n // Add error message to conversation\n var errorMessage = new TextMessage { Text = $\"Error executing tools: {ex.Message}\", Role = Role.User };\n conversationHistory.Add(errorMessage);\n }\n }\n\n if (turnCount >= maxTurns)\n {\n _logger.LogWarning(\"⚠ Max turns reached - stopping conversation\");\n }\n\n _logger.LogInformation(\"\\n=== Agentic Loop Complete ===\");\n }\n\n // ===== Helper Methods =====\n\n /// <summary>\n /// Executes a single tool call and returns the result message.\n /// </summary>\n private async Task<ToolCallResultMessage> ExecuteToolCallAsync(\n ToolCallMessage toolCall,\n IDictionary<string, Func<string, Task<string>>> handlers\n )\n {\n var toolCallId = toolCall.ToolCallId ?? $\"call_{toolCall.Index}\";\n var functionName = toolCall.FunctionName ?? \"unknown\";\n var functionArgs = toolCall.FunctionArgs ?? \"{}\";\n\n try\n {\n if (handlers.TryGetValue(functionName, out var handler))\n {\n var result = await handler(functionArgs);\n return new ToolCallResultMessage\n {\n ToolCallId = toolCallId,\n Result = result,\n Role = Role.User,\n FromAgent = toolCall.FromAgent,\n GenerationId = toolCall.GenerationId,\n };\n }\n else\n {\n _logger.LogWarning(\"No handler found for function: {FunctionName}\", functionName);\n return new ToolCallResultMessage\n {\n ToolCallId = toolCallId,\n Result = JsonSerializer.Serialize(new { error = $\"Unknown function: {functionName}\" }),\n Role = Role.User,\n FromAgent = toolCall.FromAgent,\n GenerationId = toolCall.GenerationId,\n };\n }\n }\n catch (Exception ex)\n {\n _logger.LogError(ex, \"Error executing tool call: {FunctionName}\", functionName);\n return new ToolCallResultMessage\n {\n ToolCallId = toolCallId,\n Result = JsonSerializer.Serialize(new { error = ex.Message }),\n Role = Role.User,\n FromAgent = toolCall.FromAgent,\n GenerationId = toolCall.GenerationId,\n };\n }\n }\n\n // ===== Helper Classes =====\n\n private class WeatherArgs\n {\n public string? Location { get; set; }\n public string? Unit { get; set; }\n }\n\n private class TimeArgs\n {\n public string? Timezone { get; set; }\n }\n\n private class CalculateArgs\n {\n public string? Expression { get; set; }\n }\n\n // Simple expression evaluator (for demo purposes only - unsafe in production)\n private static double EvaluateSimpleExpression(string expression)\n {\n expression = expression.Replace(\" \", \"\");\n\n // Very basic calculator - only handles +, -, *, /\n if (expression.Contains('+'))\n {\n var parts = expression.Split('+');\n return double.Parse(parts[0]) + double.Parse(parts[1]);\n }\n if (expression.Contains('-'))\n {\n var parts = expression.Split('-');\n return double.Parse(parts[0]) - double.Parse(parts[1]);\n }\n if (expression.Contains('*'))\n {\n var parts = expression.Split('*');\n return double.Parse(parts[0]) * double.Parse(parts[1]);\n }\n if (expression.Contains('/'))\n {\n var parts = expression.Split('/');\n return double.Parse(parts[0]) / double.Parse(parts[1]);\n }\n\n return double.Parse(expression);\n }\n}\n"}, "e0a2749ec68628b6e6b3026f258b8d8451deea67": {"t": 1785756742.7041762, "n": 17313, "h": "e9fca20009040825", "c": "using System.Text.Json;\nusing AchieveAi.LmDotnetTools.LmConfig.Agents;\nusing AchieveAi.LmDotnetTools.LmCore.Agents;\nusing AchieveAi.LmDotnetTools.LmCore.Messages;\nusing AchieveAi.LmDotnetTools.LmCore.Middleware;\nusing AchieveAi.LmDotnetTools.LmCore.Utils;\nusing Microsoft.Extensions.Logging;\n\nnamespace LmConfigUsageExample;\n\n/// <summary>\n/// Example demonstrating an agentic loop using OpenAI provider connected to Grok4.1\n/// with the new middleware chain architecture.\n///\n/// Middleware Chain:\n/// Provider Agent => MessageTransformationMiddleware => JsonFragmentUpdateMiddleware\n/// => MessageUpdateJoinerMiddleware => ToolCallInjectionMiddleware\n///\n/// Agentic Loop:\n/// 1. Call middleware chain (streaming)\n/// 2. Collect all messages from the stream\n/// 3. Find ToolsCallMessage in the messages\n/// 4. If found, use ToolCallExecutor to execute tool calls asynchronously\n/// 5. Add ToolsCallResultMessage to conversation\n/// 6. Loop again\n/// 7. If no ToolsCallMessage, end the loop\n/// </summary>\npublic class OpenAiGrokAgenticExample\n{\n private readonly IModelResolver _modelResolver;\n private readonly IProviderAgentFactory _agentFactory;\n private readonly ILogger<OpenAiGrokAgenticExample> _logger;\n\n public OpenAiGrokAgenticExample(\n IModelResolver modelResolver,\n IProviderAgentFactory agentFactory,\n ILogger<OpenAiGrokAgenticExample> logger\n )\n {\n _modelResolver = modelResolver;\n _agentFactory = agentFactory;\n _logger = logger;\n }\n\n public async Task RunAsync(string prompt, string modelId = \"x-ai/grok-4.1-fast\", float temperature = 0.7f, int maxTurns = 10)\n {\n _logger.LogInformation(\"=== Agentic Loop Example with {ModelId} ===\\n\", modelId);\n\n // ===== Step 1: Resolve the specified model =====\n var resolution = await _modelResolver.ResolveProviderAsync(modelId);\n if (resolution == null)\n {\n _logger.LogError(\"Failed to resolve model: {ModelId}\", modelId);\n _logger.LogWarning(\"Make sure the model exists in models.json and the provider has an API key set.\");\n _logger.LogWarning(\"\");\n _logger.LogWarning(\"Required API keys by model:\");\n _logger.LogWarning(\" grok-4.1, x-ai/* -> XAI_API_KEY\");\n _logger.LogWarning(\" gpt-4.1*, openai/* -> OPENAI_API_KEY\");\n _logger.LogWarning(\" claude-3-*, anthropic/* -> ANTHROPIC_API_KEY\");\n _logger.LogWarning(\" openrouter/* -> OPENROUTER_API_KEY\");\n _logger.LogWarning(\" deepseek/* -> DEEPSEEK_API_KEY\");\n _logger.LogWarning(\" claude-sonnet-4-5 -> ClaudeAgentSDK (no API key needed if Claude Code authenticated)\");\n _logger.LogWarning(\"\");\n _logger.LogWarning(\"Use '--list-providers' to see provider status.\");\n return;\n }\n\n _logger.LogInformation(\"✓ Resolved model: {ModelName}\", resolution.EffectiveModelName);\n _logger.LogInformation(\" Provider: {ProviderName}\", resolution.EffectiveProviderName);\n _logger.LogInformation(\" Endpoint: {Endpoint}\\n\", resolution.Connection.EndpointUrl);\n\n // ===== Step 3: Define Tools using FunctionRegistry =====\n var registry = new FunctionRegistry();\n\n // get_weather\n _ = registry.AddFunction(\n new FunctionContract\n {\n Name = \"get_weather\",\n Description = \"Get the current weather for a location\",\n Parameters =\n [\n new FunctionParameterContract\n {\n Name = \"location\",\n Description = \"The city name (e.g., 'San Francisco', 'New York')\",\n ParameterType = SchemaHelper.CreateJsonSchemaFromType(typeof(string)),\n IsRequired = true,\n },\n new FunctionParameterContract\n {\n Name = \"unit\",\n Description = \"Temperature unit: 'celsius' or 'fahrenheit'\",\n ParameterType = SchemaHelper.CreateJsonSchemaFromType(typeof(string)),\n IsRequired = false,\n },\n ],\n },\n async (args) =>\n {\n var weatherArgs = JsonSerializer.Deserialize<WeatherArgs>(args);\n _logger.LogInformation(\"[TOOL] Getting weather for {Location}\", weatherArgs?.Location);\n\n await Task.Delay(100); // Simulate API call\n\n var weather = new\n {\n location = weatherArgs?.Location,\n temperature = Random.Shared.Next(60, 85),\n unit = weatherArgs?.Unit ?? \"fahrenheit\",\n condition = new[] { \"sunny\", \"cloudy\", \"rainy\", \"partly cloudy\" }[Random.Shared.Next(4)],\n };\n\n return JsonSerializer.Serialize(weather);\n },\n providerName: \"Example\"\n );\n\n // get_time\n _ = registry.AddFunction(\n new FunctionContract\n {\n Name = \"get_time\",\n Description = \"Get the current time for a timezone\",\n Parameters =\n [\n new FunctionParameterContract\n {\n Name = \"timezone\",\n Description = \"Timezone (e.g., 'PST', 'EST', 'UTC')\",\n ParameterType = SchemaHelper.CreateJsonSchemaFromType(typeof(string)),\n IsRequired = true,\n },\n ],\n },\n async (args) =>\n {\n var timeArgs = JsonSerializer.Deserialize<TimeArgs>(args);\n _logger.LogInformation(\"[TOOL] Getting time for {Timezone}\", timeArgs?.Timezone);\n\n await Task.Delay(100); // Simulate API call\n\n var time = new\n {\n timezone = timeArgs?.Timezone,\n time = DateTime.UtcNow.ToString(\"HH:mm:ss\"),\n date = DateTime.UtcNow.ToString(\"yyyy-MM-dd\"),\n };\n\n return JsonSerializer.Serialize(time);\n },\n providerName: \"Example\"\n );\n\n // calculate\n _ = registry.AddFunction(\n new FunctionContract\n {\n Name = \"calculate\",\n Description = \"Perform a mathematical calculation\",\n Parameters =\n [\n new FunctionParameterContract\n {\n Name = \"expression\",\n Description = \"Mathematical expression (e.g., '2 + 2', '10 * 5')\",\n ParameterType = SchemaHelper.CreateJsonSchemaFromType(typeof(string)),\n IsRequired = true,\n },\n ],\n },\n async (args) =>\n {\n var calcArgs = JsonSerializer.Deserialize<CalculateArgs>(args);\n _logger.LogInformation(\"[TOOL] Calculating: {Expression}\", calcArgs?.Expression);\n\n await Task.Delay(50); // Simulate processing\n\n // Simple eval-like calculation (for demo purposes only - unsafe in production)\n try\n {\n var result = EvaluateSimpleExpression(calcArgs?.Expression ?? \"0\");\n return JsonSerializer.Serialize(new { result, expression = calcArgs?.Expression });\n }\n catch (Exception ex)\n {\n return JsonSerializer.Serialize(new { error = ex.Message });\n }\n },\n providerName: \"Example\"\n );\n\n // Build middleware and handlers from registry\n var (toolCallMiddleware, functionHandlers) = registry.BuildToolCallComponents(name: \"ToolCallInjection\");\n\n // ===== Step 4: Create Provider Agent =====\n var providerAgent = _agentFactory.CreateStreamingAgent(resolution)\n .WithMessageTransformation()\n .WithMiddleware(new JsonFragmentUpdateMiddleware())\n .WithMiddleware(new MessageUpdateJoinerMiddleware(name: \"MessageJoiner\"))\n .WithMiddleware(toolCallMiddleware);\n\n // ===== Step 6: Start Conversation =====\n var conversationHistory = new List<IMessage>\n {\n new TextMessage { Text = prompt, Role = Role.User },\n };\n\n _logger.LogInformation(\"User: {Prompt}\\n\", prompt);\n\n // ===== Step 7: Agentic Loop =====\n var turnCount = 0;\n\n while (turnCount < maxTurns)\n {\n turnCount++;\n _logger.LogInformation(\"=== Turn {Turn} ===\", turnCount);\n\n // Call LLM through middleware chain (streaming)\n var streamTask = await providerAgent.GenerateReplyStreamingAsync(\n conversationHistory,\n new GenerateReplyOptions\n {\n ModelId = modelId,\n // Temperature = temperature,\n RunId = Guid.NewGuid().ToString(),\n ParentRunId = null,\n ThreadId = Guid.NewGuid().ToString(),\n }\n );\n\n // Collect messages from stream and execute tool calls in parallel as they arrive\n var messages = new List<IMessage>();\n var textContent = new System.Text.StringBuilder();\n var pendingToolCalls = new Dictionary<string, Task<ToolCallResultMessage>>();\n\n await foreach (var message in streamTask)\n {\n messages.Add(message);\n\n // Display streaming content in real-time and handle tool calls\n switch (message)\n {\n case ReasoningMessage reasoningMsg:\n if (!string.IsNullOrEmpty(reasoningMsg.Reasoning))\n {\n Console.Write(reasoningMsg.Reasoning);\n _ = textContent.Append(reasoningMsg.Reasoning);\n }\n break;\n\n case TextMessage textMsg:\n if (!string.IsNullOrEmpty(textMsg.Text))\n {\n Console.Write(textMsg.Text);\n _ = textContent.Append(textMsg.Text);\n }\n break;\n\n case ToolCallMessage toolCall:\n // Start executing tool call immediately when we receive it\n _logger.LogInformation(\n \"[Tool Call] Starting execution: {FunctionName} (id: {ToolCallId})\",\n toolCall.FunctionName,\n toolCall.ToolCallId\n );\n\n var executionTask = ExecuteToolCallAsync(toolCall, functionHandlers);\n pendingToolCalls[toolCall.ToolCallId ?? $\"call_{pendingToolCalls.Count}\"] = executionTask;\n break;\n\n case UsageMessage usage:\n _logger.LogDebug(\n \"[Usage] Prompt: {Prompt}, Completion: {Completion}\",\n usage.Usage.PromptTokens,\n usage.Usage.CompletionTokens\n );\n break;\n }\n }\n\n Console.WriteLine(); // New line after streaming\n\n // Add all messages to conversation history\n conversationHistory.AddRange(messages);\n\n _logger.LogInformation(\"Received {Count} message(s)\", messages.Count);\n\n // ===== Step 8: Check for Tool Calls and Await Results =====\n if (pendingToolCalls.Count == 0)\n {\n // No tool calls, conversation complete\n _logger.LogInformation(\"✓ No tool calls - conversation complete\\n\");\n\n if (textContent.Length > 0)\n {\n _logger.LogInformation(\"Assistant: {Response}\", textContent.ToString());\n }\n\n break;\n }\n\n // ===== Step 9: Await Tool Results and Add to Conversation =====\n _logger.LogInformation(\"Awaiting {Count} tool call result(s)...\", pendingToolCalls.Count);\n\n try\n {\n // Await all pending tool executions\n _ = await Task.WhenAll(pendingToolCalls.Values);\n\n // Collect results and add to conversation\n foreach (var kvp in pendingToolCalls)\n {\n var result = await kvp.Value;\n var resultPreview = result.Result.Length > 100 ? result.Result[..100] + \"...\" : result.Result;\n _logger.LogInformation(\" Result for {ToolCallId}: {Result}\", kvp.Key, resultPreview);\n\n // Add each tool result to conversation history\n conversationHistory.Add(result);\n }\n\n _logger.LogInformation(\"Tool execution completed.\");\n Console.WriteLine(); // Spacing\n }\n catch (Exception ex)\n {\n _logger.LogError(ex, \"Tool execution failed\");\n\n // Add error message to conversation\n var errorMessage = new TextMessage { Text = $\"Error executing tools: {ex.Message}\", Role = Role.User };\n conversationHistory.Add(errorMessage);\n }\n }\n\n if (turnCount >= maxTurns)\n {\n _logger.LogWarning(\"⚠ Max turns reached - stopping conversation\");\n }\n\n _logger.LogInformation(\"\\n=== Agentic Loop Complete ===\");\n }\n\n // ===== Helper Methods =====\n\n /// <summary>\n /// Executes a single tool call and returns the result message.\n /// </summary>\n private async Task<ToolCallResultMessage> ExecuteToolCallAsync(\n ToolCallMessage toolCall,\n IDictionary<string, Func<string, Task<string>>> handlers\n )\n {\n var toolCallId = toolCall.ToolCallId ?? $\"call_{toolCall.Index}\";\n var functionName = toolCall.FunctionName ?? \"unknown\";\n var functionArgs = toolCall.FunctionArgs ?? \"{}\";\n\n try\n {\n if (handlers.TryGetValue(functionName, out var handler))\n {\n var result = await handler(functionArgs);\n return new ToolCallResultMessage\n {\n ToolCallId = toolCallId,\n Result = result,\n Role = Role.User,\n FromAgent = toolCall.FromAgent,\n GenerationId = toolCall.GenerationId,\n };\n }\n else\n {\n _logger.LogWarning(\"No handler found for function: {FunctionName}\", functionName);\n return new ToolCallResultMessage\n {\n ToolCallId = toolCallId,\n Result = JsonSerializer.Serialize(new { error = $\"Unknown function: {functionName}\" }),\n Role = Role.User,\n FromAgent = toolCall.FromAgent,\n GenerationId = toolCall.GenerationId,\n };\n }\n }\n catch (Exception ex)\n {\n _logger.LogError(ex, \"Error executing tool call: {FunctionName}\", functionName);\n return new ToolCallResultMessage\n {\n ToolCallId = toolCallId,\n Result = JsonSerializer.Serialize(new { error = ex.Message }),\n Role = Role.User,\n FromAgent = toolCall.FromAgent,\n GenerationId = toolCall.GenerationId,\n };\n }\n }\n\n // ===== Helper Classes =====\n\n private class WeatherArgs\n {\n public string? Location { get; set; }\n public string? Unit { get; set; }\n }\n\n private class TimeArgs\n {\n public string? Timezone { get; set; }\n }\n\n private class CalculateArgs\n {\n public string? Expression { get; set; }\n }\n\n // Simple expression evaluator (for demo purposes only - unsafe in production)\n private static double EvaluateSimpleExpression(string expression)\n {\n expression = expression.Replace(\" \", \"\");\n\n // Very basic calculator - only handles +, -, *, /\n if (expression.Contains('+'))\n {\n var parts = expression.Split('+');\n return double.Parse(parts[0]) + double.Parse(parts[1]);\n }\n if (expression.Contains('-'))\n {\n var parts = expression.Split('-');\n return double.Parse(parts[0]) - double.Parse(parts[1]);\n }\n if (expression.Contains('*'))\n {\n var parts = expression.Split('*');\n return double.Parse(parts[0]) * double.Parse(parts[1]);\n }\n if (expression.Contains('/'))\n {\n var parts = expression.Split('/');\n return double.Parse(parts[0]) / double.Parse(parts[1]);\n }\n\n return double.Parse(expression);\n }\n}\n"}, "0830e654192e285f2269a0e39a8d0683c3019c8a": {"t": 1785756744.4616446, "n": 15316, "h": "410ae0460f177633", "c": "using System.Text.Json;\nusing AchieveAi.LmDotnetTools.LmConfig.Agents;\nusing AchieveAi.LmDotnetTools.LmConfig.Services;\nusing AchieveAi.LmDotnetTools.LmCore.Agents;\nusing AchieveAi.LmDotnetTools.LmCore.Messages;\nusing AchieveAi.LmDotnetTools.LmCore.Middleware;\nusing AchieveAi.LmDotnetTools.LmCore.Utils;\nusing Microsoft.Extensions.Logging;\n\nnamespace LmConfigUsageExample;\n\n/// <summary>\n/// Example demonstrating an agentic loop using OpenAI provider connected to Grok4.1\n/// with the new middleware chain architecture.\n///\n/// Middleware Chain:\n/// Provider Agent => MessageTransformationMiddleware => JsonFragmentUpdateMiddleware\n/// => MessageUpdateJoinerMiddleware => ToolCallInjectionMiddleware\n///\n/// Agentic Loop:\n/// 1. Call middleware chain (streaming)\n/// 2. Collect all messages from the stream\n/// 3. Find ToolsCallMessage in the messages\n/// 4. If found, use ToolCallExecutor to execute tool calls asynchronously\n/// 5. Add ToolsCallResultMessage to conversation\n/// 6. Loop again\n/// 7. If no ToolsCallMessage, end the loop\n/// </summary>\npublic class OpenAiGrokAgenticExample\n{\n private readonly IModelResolver _modelResolver;\n private readonly IProviderAgentFactory _agentFactory;\n private readonly ILogger<OpenAiGrokAgenticExample> _logger;\n\n public OpenAiGrokAgenticExample(\n IModelResolver modelResolver,\n IProviderAgentFactory agentFactory,\n ILogger<OpenAiGrokAgenticExample> logger\n )\n {\n _modelResolver = modelResolver;\n _agentFactory = agentFactory;\n _logger = logger;\n }\n\n public async Task RunAsync(string prompt, string modelId = \"grok-4.1\", float temperature = 0.7f, int maxTurns = 10)\n {\n _logger.LogInformation(\"=== Agentic Loop Example with {ModelId} ===\\n\", modelId);\n\n // ===== Step 1: Resolve the specified model =====\n var resolution = await _modelResolver.ResolveProviderAsync(modelId);\n if (resolution == null)\n {\n _logger.LogError(\"Failed to resolve model: {ModelId}\", modelId);\n _logger.LogWarning(\"Make sure the model exists in models.json and the provider has an API key set.\");\n return;\n }\n\n _logger.LogInformation(\"✓ Resolved model: {ModelName}\", resolution.EffectiveModelName);\n _logger.LogInformation(\" Provider: {ProviderName}\", resolution.EffectiveProviderName);\n _logger.LogInformation(\" Endpoint: {Endpoint}\\n\", resolution.Connection.EndpointUrl);\n\n // ===== Step 3: Define Tools =====\n var functions = new[]\n {\n new FunctionContract\n {\n Name = \"get_weather\",\n Description = \"Get the current weather for a location\",\n Parameters =\n [\n new FunctionParameterContract\n {\n Name = \"location\",\n Description = \"The city name (e.g., 'San Francisco', 'New York')\",\n ParameterType = SchemaHelper.CreateJsonSchemaFromType(typeof(string)),\n IsRequired = true,\n },\n new FunctionParameterContract\n {\n Name = \"unit\",\n Description = \"Temperature unit: 'celsius' or 'fahrenheit'\",\n ParameterType = SchemaHelper.CreateJsonSchemaFromType(typeof(string)),\n IsRequired = false,\n },\n ],\n },\n new FunctionContract\n {\n Name = \"get_time\",\n Description = \"Get the current time for a timezone\",\n Parameters =\n [\n new FunctionParameterContract\n {\n Name = \"timezone\",\n Description = \"Timezone (e.g., 'PST', 'EST', 'UTC')\",\n ParameterType = SchemaHelper.CreateJsonSchemaFromType(typeof(string)),\n IsRequired = true,\n },\n ],\n },\n new FunctionContract\n {\n Name = \"calculate\",\n Description = \"Perform a mathematical calculation\",\n Parameters =\n [\n new FunctionParameterContract\n {\n Name = \"expression\",\n Description = \"Mathematical expression (e.g., '2 + 2', '10 * 5')\",\n ParameterType = SchemaHelper.CreateJsonSchemaFromType(typeof(string)),\n IsRequired = true,\n },\n ],\n },\n };\n\n var functionMap = new Dictionary<string, Func<string, Task<string>>>\n {\n [\"get_weather\"] = async (args) =>\n {\n var weatherArgs = JsonSerializer.Deserialize<WeatherArgs>(args);\n _logger.LogInformation(\"[TOOL] Getting weather for {Location}\", weatherArgs?.Location);\n\n await Task.Delay(100); // Simulate API call\n\n var weather = new\n {\n location = weatherArgs?.Location,\n temperature = Random.Shared.Next(60, 85),\n unit = weatherArgs?.Unit ?? \"fahrenheit\",\n condition = new[] { \"sunny\", \"cloudy\", \"rainy\", \"partly cloudy\" }[Random.Shared.Next(4)],\n };\n\n return JsonSerializer.Serialize(weather);\n },\n [\"get_time\"] = async (args) =>\n {\n var timeArgs = JsonSerializer.Deserialize<TimeArgs>(args);\n _logger.LogInformation(\"[TOOL] Getting time for {Timezone}\", timeArgs?.Timezone);\n\n await Task.Delay(100); // Simulate API call\n\n var time = new\n {\n timezone = timeArgs?.Timezone,\n time = DateTime.UtcNow.ToString(\"HH:mm:ss\"),\n date = DateTime.UtcNow.ToString(\"yyyy-MM-dd\"),\n };\n\n return JsonSerializer.Serialize(time);\n },\n [\"calculate\"] = async (args) =>\n {\n var calcArgs = JsonSerializer.Deserialize<CalculateArgs>(args);\n _logger.LogInformation(\"[TOOL] Calculating: {Expression}\", calcArgs?.Expression);\n\n await Task.Delay(50); // Simulate processing\n\n // Simple eval-like calculation (for demo purposes only - unsafe in production)\n try\n {\n var result = EvaluateSimpleExpression(calcArgs?.Expression ?? \"0\");\n return JsonSerializer.Serialize(new { result, expression = calcArgs?.Expression });\n }\n catch (Exception ex)\n {\n return JsonSerializer.Serialize(new { error = ex.Message });\n }\n },\n };\n\n // ===== Step 4: Create Provider Agent =====\n var providerAgent = _agentFactory.CreateStreamingAgent(resolution);\n\n // ===== Step 5: Build Middleware Chain =====\n // Chain structure (innermost to outermost):\n // Provider Agent -> ToolCallInjectionMiddleware -> MessageUpdateJoinerMiddleware\n // -> JsonFragmentUpdateMiddleware -> MessageTransformationMiddleware\n\n // Build middleware chain by wrapping one at a time (innermost first)\n IStreamingAgent agentWithMiddleware = providerAgent;\n\n // 1. ToolCallInjectionMiddleware (innermost - closest to provider)\n agentWithMiddleware = new MiddlewareWrappingStreamingAgent(\n agentWithMiddleware,\n new ToolCallInjectionMiddleware(functions: functions, name: \"ToolCallInjection\")\n );\n\n // 2. MessageUpdateJoinerMiddleware\n agentWithMiddleware = new MiddlewareWrappingStreamingAgent(\n agentWithMiddleware,\n new MessageUpdateJoinerMiddleware(name: \"MessageJoiner\")\n );\n\n // 3. JsonFragmentUpdateMiddleware\n agentWithMiddleware = new MiddlewareWrappingStreamingAgent(\n agentWithMiddleware,\n new JsonFragmentUpdateMiddleware()\n );\n\n // 4. MessageTransformationMiddleware (outermost)\n agentWithMiddleware = new MiddlewareWrappingStreamingAgent(\n agentWithMiddleware,\n new MessageTransformationMiddleware(name: \"MessageTransformation\")\n );\n\n // ===== Step 6: Start Conversation =====\n var conversationHistory = new List<IMessage>\n {\n new TextMessage { Text = prompt, Role = Role.User },\n };\n\n _logger.LogInformation(\"User: {Prompt}\\n\", prompt);\n\n // ===== Step 7: Agentic Loop =====\n int turnCount = 0;\n\n while (turnCount < maxTurns)\n {\n turnCount++;\n _logger.LogInformation(\"=== Turn {Turn} ===\", turnCount);\n\n // Call LLM through middleware chain (streaming)\n var streamTask = await agentWithMiddleware.GenerateReplyStreamingAsync(\n conversationHistory,\n new GenerateReplyOptions { ModelId = modelId, Temperature = temperature }\n );\n\n // Collect messages from stream\n var messages = new List<IMessage>();\n var textContent = new System.Text.StringBuilder();\n\n await foreach (var message in streamTask)\n {\n messages.Add(message);\n\n // Display streaming content in real-time\n switch (message)\n {\n case TextUpdateMessage textUpdate:\n Console.Write(textUpdate.Text);\n textContent.Append(textUpdate.Text);\n break;\n\n case TextMessage textMsg:\n if (!string.IsNullOrEmpty(textMsg.Text))\n {\n Console.Write(textMsg.Text);\n textContent.Append(textMsg.Text);\n }\n break;\n\n case ReasoningUpdateMessage reasoningUpdate:\n // Optionally display reasoning\n break;\n\n case ToolCallUpdateMessage toolCallUpdate:\n // Display tool call progress\n if (!string.IsNullOrEmpty(toolCallUpdate.FunctionName))\n {\n _logger.LogDebug(\n \"[Tool Update] {FunctionName}: {Args}\",\n toolCallUpdate.FunctionName,\n toolCallUpdate.FunctionArgs\n );\n }\n break;\n\n case UsageMessage usage:\n _logger.LogDebug(\n \"[Usage] Prompt: {Prompt}, Completion: {Completion}\",\n usage.Usage.PromptTokens,\n usage.Usage.CompletionTokens\n );\n break;\n }\n }\n\n Console.WriteLine(); // New line after streaming\n\n // Add all messages to conversation history\n conversationHistory.AddRange(messages);\n\n _logger.LogInformation(\"Received {Count} message(s)\", messages.Count);\n\n // ===== Step 8: Check for Tool Calls =====\n // Look for ToolsCallMessage (plural) in the messages\n var toolCallMessage = messages.OfType<ToolsCallMessage>().FirstOrDefault();\n\n if (toolCallMessage == null)\n {\n // No tool calls, conversation complete\n _logger.LogInformation(\"✓ No tool calls - conversation complete\\n\");\n\n if (textContent.Length > 0)\n {\n _logger.LogInformation(\"Assistant: {Response}\", textContent.ToString());\n }\n\n break;\n }\n\n // ===== Step 9: Execute Tools =====\n _logger.LogInformation(\"Executing {Count} tool call(s)...\", toolCallMessage.ToolCalls.Count);\n\n foreach (var toolCall in toolCallMessage.ToolCalls)\n {\n _logger.LogInformation(\n \" [{Idx}] {Name} with args: {Args}\",\n toolCall.ToolCallIdx,\n toolCall.FunctionName,\n toolCall.FunctionArgs\n );\n }\n\n try\n {\n // Execute tools using ToolCallExecutor\n var toolResult = await ToolCallExecutor.ExecuteAsync(toolCallMessage, functionMap, logger: _logger);\n\n // Log tool results\n _logger.LogInformation(\"Tool execution completed:\");\n foreach (var result in toolResult.ToolCallResults)\n {\n var resultPreview = result.Result.Length > 100 ? result.Result[..100] + \"...\" : result.Result;\n _logger.LogInformation(\" Result for {ToolCallId}: {Result}\", result.ToolCallId, resultPreview);\n }\n\n // Add tool results to conversation\n conversationHistory.Add(toolResult);\n\n Console.WriteLine(); // Spacing\n }\n catch (Exception ex)\n {\n _logger.LogError(ex, \"Tool execution failed\");\n\n // Add error message to conversation\n var errorMessage = new TextMessage { Text = $\"Error executing tools: {ex.Message}\", Role = Role.User };\n conversationHistory.Add(errorMessage);\n }\n }\n\n if (turnCount >= maxTurns)\n {\n _logger.LogWarning(\"⚠ Max turns reached - stopping conversation\");\n }\n\n _logger.LogInformation(\"\\n=== Agentic Loop Complete ===\");\n }\n\n // ===== Helper Classes =====\n\n private class WeatherArgs\n {\n public string? Location { get; set; }\n public string? Unit { get; set; }\n }\n\n private class TimeArgs\n {\n public string? Timezone { get; set; }\n }\n\n private class CalculateArgs\n {\n public string? Expression { get; set; }\n }\n\n // Simple expression evaluator (for demo purposes only - unsafe in production)\n private static double EvaluateSimpleExpression(string expression)\n {\n expression = expression.Replace(\" \", \"\");\n\n // Very basic calculator - only handles +, -, *, /\n if (expression.Contains('+'))\n {\n var parts = expression.Split('+');\n return double.Parse(parts[0]) + double.Parse(parts[1]);\n }\n if (expression.Contains('-'))\n {\n var parts = expression.Split('-');\n return double.Parse(parts[0]) - double.Parse(parts[1]);\n }\n if (expression.Contains('*'))\n {\n var parts = expression.Split('*');\n return double.Parse(parts[0]) * double.Parse(parts[1]);\n }\n if (expression.Contains('/'))\n {\n var parts = expression.Split('/');\n return double.Parse(parts[0]) / double.Parse(parts[1]);\n }\n\n return double.Parse(expression);\n }\n}\n"}, "f5a5da908baf1680e1c826a97888e7cc787823fc": {"t": 1785756746.1042001, "n": 15816, "h": "b062e03e322d8aa2", "c": "using System.Text.Json;\nusing AchieveAi.LmDotnetTools.LmConfig.Agents;\nusing AchieveAi.LmDotnetTools.LmConfig.Services;\nusing AchieveAi.LmDotnetTools.LmCore.Agents;\nusing AchieveAi.LmDotnetTools.LmCore.Messages;\nusing AchieveAi.LmDotnetTools.LmCore.Middleware;\nusing AchieveAi.LmDotnetTools.LmCore.Utils;\nusing Microsoft.Extensions.Logging;\n\nnamespace LmConfigUsageExample;\n\n/// <summary>\n/// Example demonstrating an agentic loop using OpenAI provider connected to Grok4.1\n/// with the new middleware chain architecture.\n///\n/// Middleware Chain:\n/// Provider Agent => MessageTransformationMiddleware => JsonFragmentUpdateMiddleware\n/// => MessageUpdateJoinerMiddleware => ToolCallInjectionMiddleware\n///\n/// Agentic Loop:\n/// 1. Call middleware chain (streaming)\n/// 2. Collect all messages from the stream\n/// 3. Find ToolsCallMessage in the messages\n/// 4. If found, use ToolCallExecutor to execute tool calls asynchronously\n/// 5. Add ToolsCallResultMessage to conversation\n/// 6. Loop again\n/// 7. If no ToolsCallMessage, end the loop\n/// </summary>\npublic class OpenAiGrokAgenticExample\n{\n private readonly IModelResolver _modelResolver;\n private readonly IProviderAgentFactory _agentFactory;\n private readonly ILogger<OpenAiGrokAgenticExample> _logger;\n\n public OpenAiGrokAgenticExample(\n IModelResolver modelResolver,\n IProviderAgentFactory agentFactory,\n ILogger<OpenAiGrokAgenticExample> logger\n )\n {\n _modelResolver = modelResolver;\n _agentFactory = agentFactory;\n _logger = logger;\n }\n\n public async Task RunAsync(string prompt)\n {\n _logger.LogInformation(\"=== OpenAI Grok4.1 Agentic Loop Example ===\\n\");\n\n // ===== Step 1: Check Grok4.1 availability =====\n var isAvailable = await _modelResolver.IsProviderAvailableAsync(\"xAI\");\n if (!isAvailable)\n {\n _logger.LogWarning(\"xAI provider is not available. Set XAI_API_KEY environment variable.\");\n return;\n }\n\n // ===== Step 2: Resolve Grok4.1 model =====\n var resolution = await _modelResolver.ResolveProviderAsync(\"grok-4.1\");\n if (resolution == null)\n {\n _logger.LogError(\"Failed to resolve grok-4.1 model\");\n return;\n }\n\n _logger.LogInformation(\"✓ Resolved model: {ModelName}\", resolution.EffectiveModelName);\n _logger.LogInformation(\" Provider: {ProviderName}\", resolution.EffectiveProviderName);\n _logger.LogInformation(\" Endpoint: {Endpoint}\\n\", resolution.Connection.EndpointUrl);\n\n // ===== Step 3: Define Tools =====\n var functions = new[]\n {\n new FunctionContract\n {\n Name = \"get_weather\",\n Description = \"Get the current weather for a location\",\n Parameters =\n [\n new FunctionParameterContract\n {\n Name = \"location\",\n Description = \"The city name (e.g., 'San Francisco', 'New York')\",\n ParameterType = SchemaHelper.CreateJsonSchemaFromType(typeof(string)),\n IsRequired = true\n },\n new FunctionParameterContract\n {\n Name = \"unit\",\n Description = \"Temperature unit: 'celsius' or 'fahrenheit'\",\n ParameterType = SchemaHelper.CreateJsonSchemaFromType(typeof(string)),\n IsRequired = false\n }\n ]\n },\n new FunctionContract\n {\n Name = \"get_time\",\n Description = \"Get the current time for a timezone\",\n Parameters =\n [\n new FunctionParameterContract\n {\n Name = \"timezone\",\n Description = \"Timezone (e.g., 'PST', 'EST', 'UTC')\",\n ParameterType = SchemaHelper.CreateJsonSchemaFromType(typeof(string)),\n IsRequired = true\n }\n ]\n },\n new FunctionContract\n {\n Name = \"calculate\",\n Description = \"Perform a mathematical calculation\",\n Parameters =\n [\n new FunctionParameterContract\n {\n Name = \"expression\",\n Description = \"Mathematical expression (e.g., '2 + 2', '10 * 5')\",\n ParameterType = SchemaHelper.CreateJsonSchemaFromType(typeof(string)),\n IsRequired = true\n }\n ]\n }\n };\n\n var functionMap = new Dictionary<string, Func<string, Task<string>>>\n {\n [\"get_weather\"] = async (args) =>\n {\n var weatherArgs = JsonSerializer.Deserialize<WeatherArgs>(args);\n _logger.LogInformation(\"[TOOL] Getting weather for {Location}\", weatherArgs?.Location);\n\n await Task.Delay(100); // Simulate API call\n\n var weather = new\n {\n location = weatherArgs?.Location,\n temperature = Random.Shared.Next(60, 85),\n unit = weatherArgs?.Unit ?? \"fahrenheit\",\n condition = new[] { \"sunny\", \"cloudy\", \"rainy\", \"partly cloudy\" }[Random.Shared.Next(4)]\n };\n\n return JsonSerializer.Serialize(weather);\n },\n [\"get_time\"] = async (args) =>\n {\n var timeArgs = JsonSerializer.Deserialize<TimeArgs>(args);\n _logger.LogInformation(\"[TOOL] Getting time for {Timezone}\", timeArgs?.Timezone);\n\n await Task.Delay(100); // Simulate API call\n\n var time = new\n {\n timezone = timeArgs?.Timezone,\n time = DateTime.UtcNow.ToString(\"HH:mm:ss\"),\n date = DateTime.UtcNow.ToString(\"yyyy-MM-dd\")\n };\n\n return JsonSerializer.Serialize(time);\n },\n [\"calculate\"] = async (args) =>\n {\n var calcArgs = JsonSerializer.Deserialize<CalculateArgs>(args);\n _logger.LogInformation(\"[TOOL] Calculating: {Expression}\", calcArgs?.Expression);\n\n await Task.Delay(50); // Simulate processing\n\n // Simple eval-like calculation (for demo purposes only - unsafe in production)\n try\n {\n var result = EvaluateSimpleExpression(calcArgs?.Expression ?? \"0\");\n return JsonSerializer.Serialize(new { result, expression = calcArgs?.Expression });\n }\n catch (Exception ex)\n {\n return JsonSerializer.Serialize(new { error = ex.Message });\n }\n }\n };\n\n // ===== Step 4: Create Provider Agent =====\n var providerAgent = _agentFactory.CreateStreamingAgent(resolution);\n\n // ===== Step 5: Build Middleware Chain =====\n // Chain structure (innermost to outermost):\n // Provider Agent -> ToolCallInjectionMiddleware -> MessageUpdateJoinerMiddleware\n // -> JsonFragmentUpdateMiddleware -> MessageTransformationMiddleware\n\n // Build middleware chain by wrapping one at a time (innermost first)\n IStreamingAgent agentWithMiddleware = providerAgent;\n\n // 1. ToolCallInjectionMiddleware (innermost - closest to provider)\n agentWithMiddleware = new MiddlewareWrappingStreamingAgent(\n agentWithMiddleware,\n new ToolCallInjectionMiddleware(functions: functions, name: \"ToolCallInjection\")\n );\n\n // 2. MessageUpdateJoinerMiddleware\n agentWithMiddleware = new MiddlewareWrappingStreamingAgent(\n agentWithMiddleware,\n new MessageUpdateJoinerMiddleware(name: \"MessageJoiner\")\n );\n\n // 3. JsonFragmentUpdateMiddleware\n agentWithMiddleware = new MiddlewareWrappingStreamingAgent(\n agentWithMiddleware,\n new JsonFragmentUpdateMiddleware()\n );\n\n // 4. MessageTransformationMiddleware (outermost)\n agentWithMiddleware = new MiddlewareWrappingStreamingAgent(\n agentWithMiddleware,\n new MessageTransformationMiddleware(name: \"MessageTransformation\")\n );\n\n // ===== Step 6: Start Conversation =====\n var conversationHistory = new List<IMessage>\n {\n new TextMessage\n {\n Text = prompt,\n Role = Role.User\n }\n };\n\n _logger.LogInformation(\"User: {Prompt}\\n\", prompt);\n\n // ===== Step 7: Agentic Loop =====\n int turnCount = 0;\n const int maxTurns = 10; // Safety limit\n\n while (turnCount < maxTurns)\n {\n turnCount++;\n _logger.LogInformation(\"=== Turn {Turn} ===\", turnCount);\n\n // Call LLM through middleware chain (streaming)\n var streamTask = await agentWithMiddleware.GenerateReplyStreamingAsync(\n conversationHistory,\n new GenerateReplyOptions\n {\n ModelId = \"grok-4.1\",\n Temperature = 0.7f\n }\n );\n\n // Collect messages from stream\n var messages = new List<IMessage>();\n var textContent = new System.Text.StringBuilder();\n\n await foreach (var message in streamTask)\n {\n messages.Add(message);\n\n // Display streaming content in real-time\n switch (message)\n {\n case TextUpdateMessage textUpdate:\n Console.Write(textUpdate.Text);\n textContent.Append(textUpdate.Text);\n break;\n\n case TextMessage textMsg:\n if (!string.IsNullOrEmpty(textMsg.Text))\n {\n Console.Write(textMsg.Text);\n textContent.Append(textMsg.Text);\n }\n break;\n\n case ReasoningUpdateMessage reasoningUpdate:\n // Optionally display reasoning\n break;\n\n case ToolCallUpdateMessage toolCallUpdate:\n // Display tool call progress\n if (!string.IsNullOrEmpty(toolCallUpdate.FunctionName))\n {\n _logger.LogDebug(\n \"[Tool Update] {FunctionName}: {Args}\",\n toolCallUpdate.FunctionName,\n toolCallUpdate.FunctionArgs\n );\n }\n break;\n\n case UsageMessage usage:\n _logger.LogDebug(\n \"[Usage] Prompt: {Prompt}, Completion: {Completion}\",\n usage.Usage.PromptTokens,\n usage.Usage.CompletionTokens\n );\n break;\n }\n }\n\n Console.WriteLine(); // New line after streaming\n\n // Add all messages to conversation history\n conversationHistory.AddRange(messages);\n\n _logger.LogInformation(\"Received {Count} message(s)\", messages.Count);\n\n // ===== Step 8: Check for Tool Calls =====\n // Look for ToolsCallMessage (plural) in the messages\n var toolCallMessage = messages.OfType<ToolsCallMessage>().FirstOrDefault();\n\n if (toolCallMessage == null)\n {\n // No tool calls, conversation complete\n _logger.LogInformation(\"✓ No tool calls - conversation complete\\n\");\n\n if (textContent.Length > 0)\n {\n _logger.LogInformation(\"Assistant: {Response}\", textContent.ToString());\n }\n\n break;\n }\n\n // ===== Step 9: Execute Tools =====\n _logger.LogInformation(\n \"Executing {Count} tool call(s)...\",\n toolCallMessage.ToolCalls.Count\n );\n\n foreach (var toolCall in toolCallMessage.ToolCalls)\n {\n _logger.LogInformation(\n \" [{Idx}] {Name} with args: {Args}\",\n toolCall.ToolCallIdx,\n toolCall.FunctionName,\n toolCall.FunctionArgs\n );\n }\n\n try\n {\n // Execute tools using ToolCallExecutor\n var toolResult = await ToolCallExecutor.ExecuteAsync(\n toolCallMessage,\n functionMap,\n logger: _logger\n );\n\n // Log tool results\n _logger.LogInformation(\"Tool execution completed:\");\n foreach (var result in toolResult.ToolCallResults)\n {\n var resultPreview = result.Result.Length > 100\n ? result.Result[..100] + \"...\"\n : result.Result;\n _logger.LogInformation(\" Result for {ToolCallId}: {Result}\", result.ToolCallId, resultPreview);\n }\n\n // Add tool results to conversation\n conversationHistory.Add(toolResult);\n\n Console.WriteLine(); // Spacing\n }\n catch (Exception ex)\n {\n _logger.LogError(ex, \"Tool execution failed\");\n\n // Add error message to conversation\n var errorMessage = new TextMessage\n {\n Text = $\"Error executing tools: {ex.Message}\",\n Role = Role.User\n };\n conversationHistory.Add(errorMessage);\n }\n }\n\n if (turnCount >= maxTurns)\n {\n _logger.LogWarning(\"⚠ Max turns reached - stopping conversation\");\n }\n\n _logger.LogInformation(\"\\n=== Agentic Loop Complete ===\");\n }\n\n // ===== Helper Classes =====\n\n private class WeatherArgs\n {\n public string? Location { get; set; }\n public string? Unit { get; set; }\n }\n\n private class TimeArgs\n {\n public string? Timezone { get; set; }\n }\n\n private class CalculateArgs\n {\n public string? Expression { get; set; }\n }\n\n // Simple expression evaluator (for demo purposes only - unsafe in production)\n private static double EvaluateSimpleExpression(string expression)\n {\n expression = expression.Replace(\" \", \"\");\n\n // Very basic calculator - only handles +, -, *, /\n if (expression.Contains('+'))\n {\n var parts = expression.Split('+');\n return double.Parse(parts[0]) + double.Parse(parts[1]);\n }\n if (expression.Contains('-'))\n {\n var parts = expression.Split('-');\n return double.Parse(parts[0]) - double.Parse(parts[1]);\n }\n if (expression.Contains('*'))\n {\n var parts = expression.Split('*');\n return double.Parse(parts[0]) * double.Parse(parts[1]);\n }\n if (expression.Contains('/'))\n {\n var parts = expression.Split('/');\n return double.Parse(parts[0]) / double.Parse(parts[1]);\n }\n\n return double.Parse(expression);\n }\n}\n"}, "ddd4d029a734677e4b28624bd4663e1143d96e86": {"t": 1786067473.079505, "n": 17722, "h": "362d5ae78d9ce7a6", "c": "using System.Text.Json;\nusing AchieveAi.LmDotnetTools.LmConfig.Agents;\nusing AchieveAi.LmDotnetTools.LmCore.Core;\nusing AchieveAi.LmDotnetTools.LmCore.Messages;\nusing AchieveAi.LmDotnetTools.LmCore.Middleware;\nusing AchieveAi.LmDotnetTools.LmCore.Utils;\nusing Microsoft.Extensions.Logging;\n\nnamespace LmConfigUsageExample;\n\n/// <summary>\n/// Example demonstrating an agentic loop using OpenAI provider connected to Grok4.1\n/// with the new middleware chain architecture.\n///\n/// Middleware Chain:\n/// Provider Agent => MessageTransformationMiddleware => JsonFragmentUpdateMiddleware\n/// => MessageUpdateJoinerMiddleware => ToolCallInjectionMiddleware\n///\n/// Agentic Loop:\n/// 1. Call middleware chain (streaming)\n/// 2. Collect all messages from the stream\n/// 3. Find ToolsCallMessage in the messages\n/// 4. If found, use ToolCallExecutor to execute tool calls asynchronously\n/// 5. Add ToolsCallResultMessage to conversation\n/// 6. Loop again\n/// 7. If no ToolsCallMessage, end the loop\n/// </summary>\npublic class OpenAiGrokAgenticExample\n{\n private readonly IModelResolver _modelResolver;\n private readonly IProviderAgentFactory _agentFactory;\n private readonly ILogger<OpenAiGrokAgenticExample> _logger;\n private static readonly string[] stringArray = [\"sunny\", \"cloudy\", \"rainy\", \"partly cloudy\"];\n\n public OpenAiGrokAgenticExample(\n IModelResolver modelResolver,\n IProviderAgentFactory agentFactory,\n ILogger<OpenAiGrokAgenticExample> logger\n )\n {\n _modelResolver = modelResolver;\n _agentFactory = agentFactory;\n _logger = logger;\n }\n\n public async Task RunAsync(string prompt, string modelId = \"x-ai/grok-4.1-fast\", float temperature = 0.7f, int maxTurns = 10)\n {\n _logger.LogInformation(\"=== Agentic Loop Example with {ModelId} ===\\n\", modelId);\n\n // ===== Step 1: Resolve the specified model =====\n var resolution = await _modelResolver.ResolveProviderAsync(modelId);\n if (resolution == null)\n {\n _logger.LogError(\"Failed to resolve model: {ModelId}\", modelId);\n _logger.LogWarning(\"Make sure the model exists in models.json and the provider has an API key set.\");\n _logger.LogWarning(\"\");\n _logger.LogWarning(\"Required API keys by model:\");\n _logger.LogWarning(\" grok-4.1, x-ai/* -> XAI_API_KEY\");\n _logger.LogWarning(\" gpt-4.1*, openai/* -> OPENAI_API_KEY\");\n _logger.LogWarning(\" claude-3-*, anthropic/* -> ANTHROPIC_API_KEY\");\n _logger.LogWarning(\" openrouter/* -> OPENROUTER_API_KEY\");\n _logger.LogWarning(\" deepseek/* -> DEEPSEEK_API_KEY\");\n _logger.LogWarning(\" claude-sonnet-4-5 -> ClaudeAgentSDK (no API key needed if Claude Code authenticated)\");\n _logger.LogWarning(\"\");\n _logger.LogWarning(\"Use '--list-providers' to see provider status.\");\n return;\n }\n\n _logger.LogInformation(\"✓ Resolved model: {ModelName}\", resolution.EffectiveModelName);\n _logger.LogInformation(\" Provider: {ProviderName}\", resolution.EffectiveProviderName);\n _logger.LogInformation(\" Endpoint: {Endpoint}\\n\", resolution.Connection.EndpointUrl);\n\n // ===== Step 3: Define Tools using FunctionRegistry =====\n var registry = new FunctionRegistry();\n\n // get_weather\n _ = registry.AddFunction(\n new FunctionContract\n {\n Name = \"get_weather\",\n Description = \"Get the current weather for a location\",\n Parameters =\n [\n new FunctionParameterContract\n {\n Name = \"location\",\n Description = \"The city name (e.g., 'San Francisco', 'New York')\",\n ParameterType = SchemaHelper.CreateJsonSchemaFromType(typeof(string)),\n IsRequired = true,\n },\n new FunctionParameterContract\n {\n Name = \"unit\",\n Description = \"Temperature unit: 'celsius' or 'fahrenheit'\",\n ParameterType = SchemaHelper.CreateJsonSchemaFromType(typeof(string)),\n IsRequired = false,\n },\n ],\n },\n async (args, _, _) =>\n {\n var weatherArgs = JsonSerializer.Deserialize<WeatherArgs>(args);\n _logger.LogInformation(\"[TOOL] Getting weather for {Location}\", weatherArgs?.Location);\n\n await Task.Delay(100); // Simulate API call\n\n var weather = new\n {\n location = weatherArgs?.Location,\n temperature = Random.Shared.Next(60, 85),\n unit = weatherArgs?.Unit ?? \"fahrenheit\",\n condition = stringArray[Random.Shared.Next(4)],\n };\n\n return ToolHandlerResult.FromText(JsonSerializer.Serialize(weather));\n },\n providerName: \"Example\"\n );\n\n // get_time\n _ = registry.AddFunction(\n new FunctionContract\n {\n Name = \"get_time\",\n Description = \"Get the current time for a timezone\",\n Parameters =\n [\n new FunctionParameterContract\n {\n Name = \"timezone\",\n Description = \"Timezone (e.g., 'PST', 'EST', 'UTC')\",\n ParameterType = SchemaHelper.CreateJsonSchemaFromType(typeof(string)),\n IsRequired = true,\n },\n ],\n },\n async (args, _, _) =>\n {\n var timeArgs = JsonSerializer.Deserialize<TimeArgs>(args);\n _logger.LogInformation(\"[TOOL] Getting time for {Timezone}\", timeArgs?.Timezone);\n\n await Task.Delay(100); // Simulate API call\n\n var time = new\n {\n timezone = timeArgs?.Timezone,\n time = DateTime.UtcNow.ToString(\"HH:mm:ss\"),\n date = DateTime.UtcNow.ToString(\"yyyy-MM-dd\"),\n };\n\n return ToolHandlerResult.FromText(JsonSerializer.Serialize(time));\n },\n providerName: \"Example\"\n );\n\n // calculate\n _ = registry.AddFunction(\n new FunctionContract\n {\n Name = \"calculate\",\n Description = \"Perform a mathematical calculation\",\n Parameters =\n [\n new FunctionParameterContract\n {\n Name = \"expression\",\n Description = \"Mathematical expression (e.g., '2 + 2', '10 * 5')\",\n ParameterType = SchemaHelper.CreateJsonSchemaFromType(typeof(string)),\n IsRequired = true,\n },\n ],\n },\n async (args, _, _) =>\n {\n var calcArgs = JsonSerializer.Deserialize<CalculateArgs>(args);\n _logger.LogInformation(\"[TOOL] Calculating: {Expression}\", calcArgs?.Expression);\n\n await Task.Delay(50); // Simulate processing\n\n // Simple eval-like calculation (for demo purposes only - unsafe in production)\n try\n {\n var result = EvaluateSimpleExpression(calcArgs?.Expression ?? \"0\");\n return ToolHandlerResult.FromText(JsonSerializer.Serialize(new { result, expression = calcArgs?.Expression }));\n }\n catch (Exception ex)\n {\n return ToolHandlerResult.FromError(JsonSerializer.Serialize(new { error = ex.Message }));\n }\n },\n providerName: \"Example\"\n );\n\n // Build middleware and handlers from registry\n var (toolCallMiddleware, functionHandlers) = registry.BuildToolCallComponents(name: \"ToolCallInjection\");\n\n // ===== Step 4: Create Provider Agent =====\n var providerAgent = _agentFactory.CreateStreamingAgent(resolution)\n .WithMessageTransformation()\n .WithMiddleware(new JsonFragmentUpdateMiddleware())\n .WithMiddleware(new MessageUpdateJoinerMiddleware(name: \"MessageJoiner\"))\n .WithMiddleware(toolCallMiddleware);\n\n // ===== Step 6: Start Conversation =====\n var conversationHistory = new List<IMessage>\n {\n new TextMessage { Text = prompt, Role = Role.User },\n };\n\n _logger.LogInformation(\"User: {Prompt}\\n\", prompt);\n\n // ===== Step 7: Agentic Loop =====\n var turnCount = 0;\n\n while (turnCount < maxTurns)\n {\n turnCount++;\n _logger.LogInformation(\"=== Turn {Turn} ===\", turnCount);\n\n // Call LLM through middleware chain (streaming)\n var streamTask = await providerAgent.GenerateReplyStreamingAsync(\n conversationHistory,\n new GenerateReplyOptions\n {\n ModelId = modelId,\n // Temperature = temperature,\n RunId = Guid.NewGuid().ToString(),\n ParentRunId = null,\n ThreadId = Guid.NewGuid().ToString(),\n }\n );\n\n // Collect messages from stream and execute tool calls in parallel as they arrive\n var messages = new List<IMessage>();\n var textContent = new System.Text.StringBuilder();\n var pendingToolCalls = new Dictionary<string, Task<ToolCallResultMessage>>();\n\n await foreach (var message in streamTask)\n {\n messages.Add(message);\n\n // Display streaming content in real-time and handle tool calls\n switch (message)\n {\n case ReasoningMessage reasoningMsg:\n if (!string.IsNullOrEmpty(reasoningMsg.Reasoning))\n {\n Console.Write(reasoningMsg.Reasoning);\n _ = textContent.Append(reasoningMsg.Reasoning);\n }\n break;\n\n case TextMessage textMsg:\n if (!string.IsNullOrEmpty(textMsg.Text))\n {\n Console.Write(textMsg.Text);\n _ = textContent.Append(textMsg.Text);\n }\n break;\n\n case ToolCallMessage toolCall:\n // Start executing tool call immediately when we receive it\n _logger.LogInformation(\n \"[Tool Call] Starting execution: {FunctionName} (id: {ToolCallId})\",\n toolCall.FunctionName,\n toolCall.ToolCallId\n );\n\n var executionTask = ExecuteToolCallAsync(toolCall, functionHandlers);\n pendingToolCalls[toolCall.ToolCallId ?? $\"call_{pendingToolCalls.Count}\"] = executionTask;\n break;\n\n case UsageMessage usage:\n _logger.LogDebug(\n \"[Usage] Prompt: {Prompt}, Completion: {Completion}\",\n usage.Usage.PromptTokens,\n usage.Usage.CompletionTokens\n );\n break;\n\n default:\n // Ignore other message types\n break;\n }\n }\n\n Console.WriteLine(); // New line after streaming\n\n // Add all messages to conversation history\n conversationHistory.AddRange(messages);\n\n _logger.LogInformation(\"Received {Count} message(s)\", messages.Count);\n\n // ===== Step 8: Check for Tool Calls and Await Results =====\n if (pendingToolCalls.Count == 0)\n {\n // No tool calls, conversation complete\n _logger.LogInformation(\"✓ No tool calls - conversation complete\\n\");\n\n if (textContent.Length > 0)\n {\n _logger.LogInformation(\"Assistant: {Response}\", textContent.ToString());\n }\n\n break;\n }\n\n // ===== Step 9: Await Tool Results and Add to Conversation =====\n _logger.LogInformation(\"Awaiting {Count} tool call result(s)...\", pendingToolCalls.Count);\n\n try\n {\n // Await all pending tool executions\n _ = await Task.WhenAll(pendingToolCalls.Values);\n\n // Collect results and add to conversation\n foreach (var kvp in pendingToolCalls)\n {\n var result = await kvp.Value;\n var resultPreview = result.Result.Length > 100 ? result.Result[..100] + \"...\" : result.Result;\n _logger.LogInformation(\" Result for {ToolCallId}: {Result}\", kvp.Key, resultPreview);\n\n // Add each tool result to conversation history\n conversationHistory.Add(result);\n }\n\n _logger.LogInformation(\"Tool execution completed.\");\n Console.WriteLine(); // Spacing\n }\n catch (Exception ex)\n {\n _logger.LogError(ex, \"Tool execution failed\");\n\n // Add error message to conversation\n var errorMessage = new TextMessage { Text = $\"Error executing tools: {ex.Message}\", Role = Role.User };\n conversationHistory.Add(errorMessage);\n }\n }\n\n if (turnCount >= maxTurns)\n {\n _logger.LogWarning(\"⚠ Max turns reached - stopping conversation\");\n }\n\n _logger.LogInformation(\"\\n=== Agentic Loop Complete ===\");\n }\n\n // ===== Helper Methods =====\n\n /// <summary>\n /// Executes a single tool call and returns the result message.\n /// </summary>\n private async Task<ToolCallResultMessage> ExecuteToolCallAsync(\n ToolCallMessage toolCall,\n IDictionary<string, ToolHandler> handlers\n )\n {\n var toolCallId = toolCall.ToolCallId ?? $\"call_{toolCall.Index}\";\n var functionName = toolCall.FunctionName ?? \"unknown\";\n var functionArgs = toolCall.FunctionArgs ?? \"{}\";\n\n try\n {\n if (handlers.TryGetValue(functionName, out var handler))\n {\n var ctx = new ToolCallContext { ToolCallId = toolCall.ToolCallId };\n var result = await handler(functionArgs, ctx, CancellationToken.None);\n return new ToolCallResultMessage\n {\n ToolCallId = toolCallId,\n Result = result.ResultText,\n Role = Role.User,\n FromAgent = toolCall.FromAgent,\n GenerationId = toolCall.GenerationId,\n };\n }\n else\n {\n _logger.LogWarning(\"No handler found for function: {FunctionName}\", functionName);\n return new ToolCallResultMessage\n {\n ToolCallId = toolCallId,\n Result = JsonSerializer.Serialize(new { error = $\"Unknown function: {functionName}\" }),\n Role = Role.User,\n FromAgent = toolCall.FromAgent,\n GenerationId = toolCall.GenerationId,\n };\n }\n }\n catch (Exception ex)\n {\n _logger.LogError(ex, \"Error executing tool call: {FunctionName}\", functionName);\n return new ToolCallResultMessage\n {\n ToolCallId = toolCallId,\n Result = JsonSerializer.Serialize(new { error = ex.Message }),\n Role = Role.User,\n FromAgent = toolCall.FromAgent,\n GenerationId = toolCall.GenerationId,\n };\n }\n }\n\n // ===== Helper Classes =====\n\n private class WeatherArgs\n {\n public string? Location { get; set; }\n public string? Unit { get; set; }\n }\n\n private class TimeArgs\n {\n public string? Timezone { get; set; }\n }\n\n private class CalculateArgs\n {\n public string? Expression { get; set; }\n }\n\n // Simple expression evaluator (for demo purposes only - unsafe in production)\n private static double EvaluateSimpleExpression(string expression)\n {\n expression = expression.Replace(\" \", \"\");\n\n // Very basic calculator - only handles +, -, *, /\n if (expression.Contains('+'))\n {\n var parts = expression.Split('+');\n return double.Parse(parts[0]) + double.Parse(parts[1]);\n }\n if (expression.Contains('-'))\n {\n var parts = expression.Split('-');\n return double.Parse(parts[0]) - double.Parse(parts[1]);\n }\n if (expression.Contains('*'))\n {\n var parts = expression.Split('*');\n return double.Parse(parts[0]) * double.Parse(parts[1]);\n }\n if (expression.Contains('/'))\n {\n var parts = expression.Split('/');\n return double.Parse(parts[0]) / double.Parse(parts[1]);\n }\n\n return double.Parse(expression);\n }\n}\n"}, "3a25297b94f73fcda85e6e48bc8b48c2ee8e662d": {"t": 1786068324.15928, "n": 17722, "h": "362d5ae78d9ce7a6", "c": "using System.Text.Json;\nusing AchieveAi.LmDotnetTools.LmConfig.Agents;\nusing AchieveAi.LmDotnetTools.LmCore.Core;\nusing AchieveAi.LmDotnetTools.LmCore.Messages;\nusing AchieveAi.LmDotnetTools.LmCore.Middleware;\nusing AchieveAi.LmDotnetTools.LmCore.Utils;\nusing Microsoft.Extensions.Logging;\n\nnamespace LmConfigUsageExample;\n\n/// <summary>\n/// Example demonstrating an agentic loop using OpenAI provider connected to Grok4.1\n/// with the new middleware chain architecture.\n///\n/// Middleware Chain:\n/// Provider Agent => MessageTransformationMiddleware => JsonFragmentUpdateMiddleware\n/// => MessageUpdateJoinerMiddleware => ToolCallInjectionMiddleware\n///\n/// Agentic Loop:\n/// 1. Call middleware chain (streaming)\n/// 2. Collect all messages from the stream\n/// 3. Find ToolsCallMessage in the messages\n/// 4. If found, use ToolCallExecutor to execute tool calls asynchronously\n/// 5. Add ToolsCallResultMessage to conversation\n/// 6. Loop again\n/// 7. If no ToolsCallMessage, end the loop\n/// </summary>\npublic class OpenAiGrokAgenticExample\n{\n private readonly IModelResolver _modelResolver;\n private readonly IProviderAgentFactory _agentFactory;\n private readonly ILogger<OpenAiGrokAgenticExample> _logger;\n private static readonly string[] stringArray = [\"sunny\", \"cloudy\", \"rainy\", \"partly cloudy\"];\n\n public OpenAiGrokAgenticExample(\n IModelResolver modelResolver,\n IProviderAgentFactory agentFactory,\n ILogger<OpenAiGrokAgenticExample> logger\n )\n {\n _modelResolver = modelResolver;\n _agentFactory = agentFactory;\n _logger = logger;\n }\n\n public async Task RunAsync(string prompt, string modelId = \"x-ai/grok-4.1-fast\", float temperature = 0.7f, int maxTurns = 10)\n {\n _logger.LogInformation(\"=== Agentic Loop Example with {ModelId} ===\\n\", modelId);\n\n // ===== Step 1: Resolve the specified model =====\n var resolution = await _modelResolver.ResolveProviderAsync(modelId);\n if (resolution == null)\n {\n _logger.LogError(\"Failed to resolve model: {ModelId}\", modelId);\n _logger.LogWarning(\"Make sure the model exists in models.json and the provider has an API key set.\");\n _logger.LogWarning(\"\");\n _logger.LogWarning(\"Required API keys by model:\");\n _logger.LogWarning(\" grok-4.1, x-ai/* -> XAI_API_KEY\");\n _logger.LogWarning(\" gpt-4.1*, openai/* -> OPENAI_API_KEY\");\n _logger.LogWarning(\" claude-3-*, anthropic/* -> ANTHROPIC_API_KEY\");\n _logger.LogWarning(\" openrouter/* -> OPENROUTER_API_KEY\");\n _logger.LogWarning(\" deepseek/* -> DEEPSEEK_API_KEY\");\n _logger.LogWarning(\" claude-sonnet-4-5 -> ClaudeAgentSDK (no API key needed if Claude Code authenticated)\");\n _logger.LogWarning(\"\");\n _logger.LogWarning(\"Use '--list-providers' to see provider status.\");\n return;\n }\n\n _logger.LogInformation(\"✓ Resolved model: {ModelName}\", resolution.EffectiveModelName);\n _logger.LogInformation(\" Provider: {ProviderName}\", resolution.EffectiveProviderName);\n _logger.LogInformation(\" Endpoint: {Endpoint}\\n\", resolution.Connection.EndpointUrl);\n\n // ===== Step 3: Define Tools using FunctionRegistry =====\n var registry = new FunctionRegistry();\n\n // get_weather\n _ = registry.AddFunction(\n new FunctionContract\n {\n Name = \"get_weather\",\n Description = \"Get the current weather for a location\",\n Parameters =\n [\n new FunctionParameterContract\n {\n Name = \"location\",\n Description = \"The city name (e.g., 'San Francisco', 'New York')\",\n ParameterType = SchemaHelper.CreateJsonSchemaFromType(typeof(string)),\n IsRequired = true,\n },\n new FunctionParameterContract\n {\n Name = \"unit\",\n Description = \"Temperature unit: 'celsius' or 'fahrenheit'\",\n ParameterType = SchemaHelper.CreateJsonSchemaFromType(typeof(string)),\n IsRequired = false,\n },\n ],\n },\n async (args, _, _) =>\n {\n var weatherArgs = JsonSerializer.Deserialize<WeatherArgs>(args);\n _logger.LogInformation(\"[TOOL] Getting weather for {Location}\", weatherArgs?.Location);\n\n await Task.Delay(100); // Simulate API call\n\n var weather = new\n {\n location = weatherArgs?.Location,\n temperature = Random.Shared.Next(60, 85),\n unit = weatherArgs?.Unit ?? \"fahrenheit\",\n condition = stringArray[Random.Shared.Next(4)],\n };\n\n return ToolHandlerResult.FromText(JsonSerializer.Serialize(weather));\n },\n providerName: \"Example\"\n );\n\n // get_time\n _ = registry.AddFunction(\n new FunctionContract\n {\n Name = \"get_time\",\n Description = \"Get the current time for a timezone\",\n Parameters =\n [\n new FunctionParameterContract\n {\n Name = \"timezone\",\n Description = \"Timezone (e.g., 'PST', 'EST', 'UTC')\",\n ParameterType = SchemaHelper.CreateJsonSchemaFromType(typeof(string)),\n IsRequired = true,\n },\n ],\n },\n async (args, _, _) =>\n {\n var timeArgs = JsonSerializer.Deserialize<TimeArgs>(args);\n _logger.LogInformation(\"[TOOL] Getting time for {Timezone}\", timeArgs?.Timezone);\n\n await Task.Delay(100); // Simulate API call\n\n var time = new\n {\n timezone = timeArgs?.Timezone,\n time = DateTime.UtcNow.ToString(\"HH:mm:ss\"),\n date = DateTime.UtcNow.ToString(\"yyyy-MM-dd\"),\n };\n\n return ToolHandlerResult.FromText(JsonSerializer.Serialize(time));\n },\n providerName: \"Example\"\n );\n\n // calculate\n _ = registry.AddFunction(\n new FunctionContract\n {\n Name = \"calculate\",\n Description = \"Perform a mathematical calculation\",\n Parameters =\n [\n new FunctionParameterContract\n {\n Name = \"expression\",\n Description = \"Mathematical expression (e.g., '2 + 2', '10 * 5')\",\n ParameterType = SchemaHelper.CreateJsonSchemaFromType(typeof(string)),\n IsRequired = true,\n },\n ],\n },\n async (args, _, _) =>\n {\n var calcArgs = JsonSerializer.Deserialize<CalculateArgs>(args);\n _logger.LogInformation(\"[TOOL] Calculating: {Expression}\", calcArgs?.Expression);\n\n await Task.Delay(50); // Simulate processing\n\n // Simple eval-like calculation (for demo purposes only - unsafe in production)\n try\n {\n var result = EvaluateSimpleExpression(calcArgs?.Expression ?? \"0\");\n return ToolHandlerResult.FromText(JsonSerializer.Serialize(new { result, expression = calcArgs?.Expression }));\n }\n catch (Exception ex)\n {\n return ToolHandlerResult.FromError(JsonSerializer.Serialize(new { error = ex.Message }));\n }\n },\n providerName: \"Example\"\n );\n\n // Build middleware and handlers from registry\n var (toolCallMiddleware, functionHandlers) = registry.BuildToolCallComponents(name: \"ToolCallInjection\");\n\n // ===== Step 4: Create Provider Agent =====\n var providerAgent = _agentFactory.CreateStreamingAgent(resolution)\n .WithMessageTransformation()\n .WithMiddleware(new JsonFragmentUpdateMiddleware())\n .WithMiddleware(new MessageUpdateJoinerMiddleware(name: \"MessageJoiner\"))\n .WithMiddleware(toolCallMiddleware);\n\n // ===== Step 6: Start Conversation =====\n var conversationHistory = new List<IMessage>\n {\n new TextMessage { Text = prompt, Role = Role.User },\n };\n\n _logger.LogInformation(\"User: {Prompt}\\n\", prompt);\n\n // ===== Step 7: Agentic Loop =====\n var turnCount = 0;\n\n while (turnCount < maxTurns)\n {\n turnCount++;\n _logger.LogInformation(\"=== Turn {Turn} ===\", turnCount);\n\n // Call LLM through middleware chain (streaming)\n var streamTask = await providerAgent.GenerateReplyStreamingAsync(\n conversationHistory,\n new GenerateReplyOptions\n {\n ModelId = modelId,\n // Temperature = temperature,\n RunId = Guid.NewGuid().ToString(),\n ParentRunId = null,\n ThreadId = Guid.NewGuid().ToString(),\n }\n );\n\n // Collect messages from stream and execute tool calls in parallel as they arrive\n var messages = new List<IMessage>();\n var textContent = new System.Text.StringBuilder();\n var pendingToolCalls = new Dictionary<string, Task<ToolCallResultMessage>>();\n\n await foreach (var message in streamTask)\n {\n messages.Add(message);\n\n // Display streaming content in real-time and handle tool calls\n switch (message)\n {\n case ReasoningMessage reasoningMsg:\n if (!string.IsNullOrEmpty(reasoningMsg.Reasoning))\n {\n Console.Write(reasoningMsg.Reasoning);\n _ = textContent.Append(reasoningMsg.Reasoning);\n }\n break;\n\n case TextMessage textMsg:\n if (!string.IsNullOrEmpty(textMsg.Text))\n {\n Console.Write(textMsg.Text);\n _ = textContent.Append(textMsg.Text);\n }\n break;\n\n case ToolCallMessage toolCall:\n // Start executing tool call immediately when we receive it\n _logger.LogInformation(\n \"[Tool Call] Starting execution: {FunctionName} (id: {ToolCallId})\",\n toolCall.FunctionName,\n toolCall.ToolCallId\n );\n\n var executionTask = ExecuteToolCallAsync(toolCall, functionHandlers);\n pendingToolCalls[toolCall.ToolCallId ?? $\"call_{pendingToolCalls.Count}\"] = executionTask;\n break;\n\n case UsageMessage usage:\n _logger.LogDebug(\n \"[Usage] Prompt: {Prompt}, Completion: {Completion}\",\n usage.Usage.PromptTokens,\n usage.Usage.CompletionTokens\n );\n break;\n\n default:\n // Ignore other message types\n break;\n }\n }\n\n Console.WriteLine(); // New line after streaming\n\n // Add all messages to conversation history\n conversationHistory.AddRange(messages);\n\n _logger.LogInformation(\"Received {Count} message(s)\", messages.Count);\n\n // ===== Step 8: Check for Tool Calls and Await Results =====\n if (pendingToolCalls.Count == 0)\n {\n // No tool calls, conversation complete\n _logger.LogInformation(\"✓ No tool calls - conversation complete\\n\");\n\n if (textContent.Length > 0)\n {\n _logger.LogInformation(\"Assistant: {Response}\", textContent.ToString());\n }\n\n break;\n }\n\n // ===== Step 9: Await Tool Results and Add to Conversation =====\n _logger.LogInformation(\"Awaiting {Count} tool call result(s)...\", pendingToolCalls.Count);\n\n try\n {\n // Await all pending tool executions\n _ = await Task.WhenAll(pendingToolCalls.Values);\n\n // Collect results and add to conversation\n foreach (var kvp in pendingToolCalls)\n {\n var result = await kvp.Value;\n var resultPreview = result.Result.Length > 100 ? result.Result[..100] + \"...\" : result.Result;\n _logger.LogInformation(\" Result for {ToolCallId}: {Result}\", kvp.Key, resultPreview);\n\n // Add each tool result to conversation history\n conversationHistory.Add(result);\n }\n\n _logger.LogInformation(\"Tool execution completed.\");\n Console.WriteLine(); // Spacing\n }\n catch (Exception ex)\n {\n _logger.LogError(ex, \"Tool execution failed\");\n\n // Add error message to conversation\n var errorMessage = new TextMessage { Text = $\"Error executing tools: {ex.Message}\", Role = Role.User };\n conversationHistory.Add(errorMessage);\n }\n }\n\n if (turnCount >= maxTurns)\n {\n _logger.LogWarning(\"⚠ Max turns reached - stopping conversation\");\n }\n\n _logger.LogInformation(\"\\n=== Agentic Loop Complete ===\");\n }\n\n // ===== Helper Methods =====\n\n /// <summary>\n /// Executes a single tool call and returns the result message.\n /// </summary>\n private async Task<ToolCallResultMessage> ExecuteToolCallAsync(\n ToolCallMessage toolCall,\n IDictionary<string, ToolHandler> handlers\n )\n {\n var toolCallId = toolCall.ToolCallId ?? $\"call_{toolCall.Index}\";\n var functionName = toolCall.FunctionName ?? \"unknown\";\n var functionArgs = toolCall.FunctionArgs ?? \"{}\";\n\n try\n {\n if (handlers.TryGetValue(functionName, out var handler))\n {\n var ctx = new ToolCallContext { ToolCallId = toolCall.ToolCallId };\n var result = await handler(functionArgs, ctx, CancellationToken.None);\n return new ToolCallResultMessage\n {\n ToolCallId = toolCallId,\n Result = result.ResultText,\n Role = Role.User,\n FromAgent = toolCall.FromAgent,\n GenerationId = toolCall.GenerationId,\n };\n }\n else\n {\n _logger.LogWarning(\"No handler found for function: {FunctionName}\", functionName);\n return new ToolCallResultMessage\n {\n ToolCallId = toolCallId,\n Result = JsonSerializer.Serialize(new { error = $\"Unknown function: {functionName}\" }),\n Role = Role.User,\n FromAgent = toolCall.FromAgent,\n GenerationId = toolCall.GenerationId,\n };\n }\n }\n catch (Exception ex)\n {\n _logger.LogError(ex, \"Error executing tool call: {FunctionName}\", functionName);\n return new ToolCallResultMessage\n {\n ToolCallId = toolCallId,\n Result = JsonSerializer.Serialize(new { error = ex.Message }),\n Role = Role.User,\n FromAgent = toolCall.FromAgent,\n GenerationId = toolCall.GenerationId,\n };\n }\n }\n\n // ===== Helper Classes =====\n\n private class WeatherArgs\n {\n public string? Location { get; set; }\n public string? Unit { get; set; }\n }\n\n private class TimeArgs\n {\n public string? Timezone { get; set; }\n }\n\n private class CalculateArgs\n {\n public string? Expression { get; set; }\n }\n\n // Simple expression evaluator (for demo purposes only - unsafe in production)\n private static double EvaluateSimpleExpression(string expression)\n {\n expression = expression.Replace(\" \", \"\");\n\n // Very basic calculator - only handles +, -, *, /\n if (expression.Contains('+'))\n {\n var parts = expression.Split('+');\n return double.Parse(parts[0]) + double.Parse(parts[1]);\n }\n if (expression.Contains('-'))\n {\n var parts = expression.Split('-');\n return double.Parse(parts[0]) - double.Parse(parts[1]);\n }\n if (expression.Contains('*'))\n {\n var parts = expression.Split('*');\n return double.Parse(parts[0]) * double.Parse(parts[1]);\n }\n if (expression.Contains('/'))\n {\n var parts = expression.Split('/');\n return double.Parse(parts[0]) / double.Parse(parts[1]);\n }\n\n return double.Parse(expression);\n }\n}\n"}}, "tests/LmConfig.Tests/UnifiedAgentTests.cs": {"5df9a37e189f520689e30ebef085dd06c50d907c": {"t": 1785745826.36667, "n": 20653, "h": "2ac87503efdbef2b", "c": "using AchieveAi.LmDotnetTools.LmConfig.Agents;\nusing AchieveAi.LmDotnetTools.LmConfig.Capabilities;\nusing AchieveAi.LmDotnetTools.LmConfig.Models;\nusing AchieveAi.LmDotnetTools.LmCore.Agents;\nusing AchieveAi.LmDotnetTools.LmCore.Core;\nusing AchieveAi.LmDotnetTools.LmCore.Messages;\nusing Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.Options;\nusing Moq;\n\nnamespace LmConfig.Tests;\n\npublic class UnifiedAgentTests\n{\n private static ServiceCollection CreateServices(AppConfig config)\n {\n var services = new ServiceCollection();\n services.AddLogging();\n services.AddSingleton(Options.Create(config));\n services.AddSingleton<\n AchieveAi.LmDotnetTools.LmConfig.Http.IHttpHandlerBuilder,\n AchieveAi.LmDotnetTools.LmConfig.Http.HandlerBuilder>();\n return services;\n }\n\n private static async IAsyncEnumerable<IMessage> CreateEmptyAsyncEnumerable()\n {\n await Task.CompletedTask; // Make it async\n yield break; // Return empty sequence\n }\n\n [Fact]\n public void UnifiedAgent_ShouldImplementIAgent()\n {\n // Arrange\n var config = new AppConfig\n {\n Models =\n [\n new ModelConfig\n {\n Id = \"test-model\",\n Capabilities = new ModelCapabilities\n {\n TokenLimits = new TokenLimits\n {\n MaxContextTokens = 4000,\n MaxOutputTokens = 1000,\n RecommendedMaxPromptTokens = 3000,\n },\n },\n Providers =\n [\n new ProviderConfig\n {\n Name = \"TestProvider\",\n ModelName = \"test-model-name\",\n Priority = 1,\n Pricing = new PricingConfig { PromptPerMillion = 1.0, CompletionPerMillion = 2.0 },\n Tags = [\"test\"],\n },\n ],\n },\n ],\n ProviderRegistry = new Dictionary<string, ProviderConnectionInfo>\n {\n [\"TestProvider\"] = new ProviderConnectionInfo\n {\n EndpointUrl = \"https://test.example.com\",\n ApiKeyEnvironmentVariable = \"TEST_API_KEY\",\n Compatibility = \"OpenAI\",\n },\n },\n };\n\n var services = CreateServices(config);\n services.AddSingleton<IModelResolver, ModelResolver>();\n services.AddSingleton<IProviderAgentFactory, ProviderAgentFactory>();\n services.AddSingleton<UnifiedAgent>();\n\n var serviceProvider = services.BuildServiceProvider();\n\n // Act\n var agent = serviceProvider.GetRequiredService<UnifiedAgent>();\n\n // Assert\n Assert.NotNull(agent);\n Assert.IsAssignableFrom<IAgent>(agent);\n Assert.IsAssignableFrom<IStreamingAgent>(agent);\n }\n\n [Fact]\n public async Task ModelResolver_ShouldResolveProvider()\n {\n // Arrange\n var config = new AppConfig\n {\n Models =\n [\n new ModelConfig\n {\n Id = \"test-model\",\n Capabilities = new ModelCapabilities\n {\n TokenLimits = new TokenLimits\n {\n MaxContextTokens = 4000,\n MaxOutputTokens = 1000,\n RecommendedMaxPromptTokens = 3000,\n },\n },\n Providers =\n [\n new ProviderConfig\n {\n Name = \"TestProvider\",\n ModelName = \"test-model-name\",\n Priority = 1,\n Pricing = new PricingConfig { PromptPerMillion = 1.0, CompletionPerMillion = 2.0 },\n Tags = [\"test\"],\n },\n ],\n },\n ],\n ProviderRegistry = new Dictionary<string, ProviderConnectionInfo>\n {\n [\"TestProvider\"] = new ProviderConnectionInfo\n {\n EndpointUrl = \"https://test.example.com\",\n ApiKeyEnvironmentVariable = \"TEST_API_KEY\",\n Compatibility = \"OpenAI\",\n },\n },\n };\n\n // Set up the required environment variable for the test\n Environment.SetEnvironmentVariable(\"TEST_API_KEY\", \"test-api-key\");\n\n try\n {\n var services = CreateServices(config);\n services.AddSingleton<IModelResolver, ModelResolver>();\n\n var serviceProvider = services.BuildServiceProvider();\n var resolver = serviceProvider.GetRequiredService<IModelResolver>();\n\n // Act\n var resolution = await resolver.ResolveProviderAsync(\"test-model\");\n\n // Assert\n Assert.NotNull(resolution);\n Assert.Equal(\"test-model\", resolution.Model.Id);\n Assert.Equal(\"TestProvider\", resolution.Provider.Name);\n Assert.Equal(\"test-model-name\", resolution.Provider.ModelName);\n Assert.NotNull(resolution.Connection);\n Assert.Equal(\"https://test.example.com\", resolution.Connection.EndpointUrl);\n }\n finally\n {\n // Clean up the environment variable\n Environment.SetEnvironmentVariable(\"TEST_API_KEY\", null);\n }\n }\n\n [Fact]\n public async Task ModelResolver_ShouldReturnNullForUnknownModel()\n {\n // Arrange\n var config = new AppConfig\n {\n Models = [],\n ProviderRegistry = new Dictionary<string, ProviderConnectionInfo>(),\n };\n\n var services = CreateServices(config);\n services.AddSingleton<IModelResolver, ModelResolver>();\n\n var serviceProvider = services.BuildServiceProvider();\n var resolver = serviceProvider.GetRequiredService<IModelResolver>();\n\n // Act\n var resolution = await resolver.ResolveProviderAsync(\"unknown-model\");\n\n // Assert\n Assert.Null(resolution);\n }\n\n [Fact]\n public async Task UnifiedAgent_ShouldUpdateModelIdWhenForwardingToProviderAgent()\n {\n // Arrange\n var config = new AppConfig\n {\n Models =\n [\n new ModelConfig\n {\n Id = \"gpt-4.1\",\n Capabilities = new ModelCapabilities\n {\n TokenLimits = new TokenLimits\n {\n MaxContextTokens = 4000,\n MaxOutputTokens = 1000,\n RecommendedMaxPromptTokens = 3000,\n },\n },\n Providers =\n [\n new ProviderConfig\n {\n Name = \"OpenRouter\",\n ModelName = \"openai/gpt-4.1\", // Different from the model ID\n Priority = 1,\n Pricing = new PricingConfig { PromptPerMillion = 2.0, CompletionPerMillion = 8.0 },\n Tags = [\"openai-compatible\"],\n },\n ],\n },\n ],\n ProviderRegistry = new Dictionary<string, ProviderConnectionInfo>\n {\n [\"OpenRouter\"] = new ProviderConnectionInfo\n {\n EndpointUrl = \"https://openrouter.ai/api/v1\",\n ApiKeyEnvironmentVariable = \"OPENROUTER_API_KEY\",\n Compatibility = \"OpenAI\",\n },\n },\n };\n\n var services = CreateServices(config);\n\n // Create a mock model resolver that returns a predefined resolution\n var mockModelResolver = new Mock<IModelResolver>();\n var expectedResolution = new ProviderResolution\n {\n Model = config.Models[0],\n Provider = config.Models[0].Providers[0],\n Connection = config.ProviderRegistry[\"OpenRouter\"],\n };\n\n mockModelResolver\n .Setup(r =>\n r.ResolveProviderAsync(\n It.IsAny<string>(),\n It.IsAny<ProviderSelectionCriteria>(),\n It.IsAny<CancellationToken>()\n )\n )\n .ReturnsAsync(expectedResolution);\n\n // Create a mock agent factory that captures the options passed to it\n var mockAgentFactory = new Mock<IProviderAgentFactory>();\n var mockAgent = new Mock<IStreamingAgent>();\n\n GenerateReplyOptions? capturedOptions = null;\n\n mockAgent\n .Setup(a =>\n a.GenerateReplyAsync(\n It.IsAny<IEnumerable<IMessage>>(),\n It.IsAny<GenerateReplyOptions>(),\n It.IsAny<CancellationToken>()\n )\n )\n .Callback<IEnumerable<IMessage>, GenerateReplyOptions?, CancellationToken>(\n (messages, options, ct) => capturedOptions = options\n )\n .ReturnsAsync(\n [\n new TextMessage { Role = Role.Assistant, Text = \"Test response\" },\n ]\n );\n\n mockAgentFactory.Setup(f => f.CreateAgent(It.IsAny<ProviderResolution>())).Returns(mockAgent.Object);\n\n services.AddSingleton(mockModelResolver.Object);\n services.AddSingleton(mockAgentFactory.Object);\n services.AddSingleton<UnifiedAgent>();\n\n var serviceProvider = services.BuildServiceProvider();\n var unifiedAgent = serviceProvider.GetRequiredService<UnifiedAgent>();\n\n var messages = new List<IMessage>\n {\n new TextMessage { Role = Role.User, Text = \"Hello, world!\" },\n };\n\n var originalOptions = new GenerateReplyOptions\n {\n ModelId = \"gpt-4.1\", // User specifies the logical model ID\n Temperature = 0.7f,\n };\n\n // Act\n await unifiedAgent.GenerateReplyAsync(messages, originalOptions);\n\n // Assert\n Assert.NotNull(capturedOptions);\n Assert.Equal(\"openai/gpt-4.1\", capturedOptions.ModelId); // Should be updated to provider's model name\n Assert.Equal(0.7f, capturedOptions.Temperature); // Other properties should be preserved\n\n // Verify the mock was called\n mockAgent.Verify(\n a =>\n a.GenerateReplyAsync(\n It.IsAny<IEnumerable<IMessage>>(),\n It.Is<GenerateReplyOptions>(o => o.ModelId == \"openai/gpt-4.1\"),\n It.IsAny<CancellationToken>()\n ),\n Times.Once\n );\n }\n\n [Fact]\n public async Task UnifiedAgent_ShouldUpdateModelIdForStreamingRequests()\n {\n // Arrange\n var config = new AppConfig\n {\n Models =\n [\n new ModelConfig\n {\n Id = \"claude-3-sonnet\",\n Capabilities = new ModelCapabilities\n {\n TokenLimits = new TokenLimits\n {\n MaxContextTokens = 4000,\n MaxOutputTokens = 1000,\n RecommendedMaxPromptTokens = 3000,\n },\n },\n Providers =\n [\n new ProviderConfig\n {\n Name = \"OpenRouter\",\n ModelName = \"anthropic/claude-3-sonnet\", // Different from the model ID\n Priority = 1,\n Pricing = new PricingConfig { PromptPerMillion = 3.0, CompletionPerMillion = 15.0 },\n Tags = [\"openai-compatible\"],\n },\n ],\n },\n ],\n ProviderRegistry = new Dictionary<string, ProviderConnectionInfo>\n {\n [\"OpenRouter\"] = new ProviderConnectionInfo\n {\n EndpointUrl = \"https://openrouter.ai/api/v1\",\n ApiKeyEnvironmentVariable = \"OPENROUTER_API_KEY\",\n Compatibility = \"OpenAI\",\n },\n },\n };\n\n var services = CreateServices(config);\n\n // Create a mock model resolver that returns a predefined resolution\n var mockModelResolver = new Mock<IModelResolver>();\n var expectedResolution = new ProviderResolution\n {\n Model = config.Models[0],\n Provider = config.Models[0].Providers[0],\n Connection = config.ProviderRegistry[\"OpenRouter\"],\n };\n\n mockModelResolver\n .Setup(r =>\n r.ResolveProviderAsync(\n It.IsAny<string>(),\n It.IsAny<ProviderSelectionCriteria>(),\n It.IsAny<CancellationToken>()\n )\n )\n .ReturnsAsync(expectedResolution);\n\n // Create a mock agent factory that captures the options passed to it\n var mockAgentFactory = new Mock<IProviderAgentFactory>();\n var mockAgent = new Mock<IStreamingAgent>();\n\n GenerateReplyOptions? capturedOptions = null;\n\n mockAgent\n .Setup(a =>\n a.GenerateReplyStreamingAsync(\n It.IsAny<IEnumerable<IMessage>>(),\n It.IsAny<GenerateReplyOptions>(),\n It.IsAny<CancellationToken>()\n )\n )\n .Callback<IEnumerable<IMessage>, GenerateReplyOptions?, CancellationToken>(\n (messages, options, ct) => capturedOptions = options\n )\n .ReturnsAsync(CreateEmptyAsyncEnumerable());\n\n mockAgentFactory.Setup(f => f.CreateStreamingAgent(It.IsAny<ProviderResolution>())).Returns(mockAgent.Object);\n\n services.AddSingleton(mockModelResolver.Object);\n services.AddSingleton(mockAgentFactory.Object);\n services.AddSingleton<UnifiedAgent>();\n\n var serviceProvider = services.BuildServiceProvider();\n var unifiedAgent = serviceProvider.GetRequiredService<UnifiedAgent>();\n\n var messages = new List<IMessage>\n {\n new TextMessage { Role = Role.User, Text = \"Hello, world!\" },\n };\n\n var originalOptions = new GenerateReplyOptions\n {\n ModelId = \"claude-3-sonnet\", // User specifies the logical model ID\n Temperature = 0.5f,\n MaxToken = 2000,\n };\n\n // Act\n await unifiedAgent.GenerateReplyStreamingAsync(messages, originalOptions);\n\n // Assert\n Assert.NotNull(capturedOptions);\n Assert.Equal(\"anthropic/claude-3-sonnet\", capturedOptions.ModelId); // Should be updated to provider's model name\n Assert.Equal(0.5f, capturedOptions.Temperature); // Other properties should be preserved\n Assert.Equal(2000, capturedOptions.MaxToken); // Other properties should be preserved\n\n // Verify the mock was called\n mockAgent.Verify(\n a =>\n a.GenerateReplyStreamingAsync(\n It.IsAny<IEnumerable<IMessage>>(),\n It.Is<GenerateReplyOptions>(o => o.ModelId == \"anthropic/claude-3-sonnet\"),\n It.IsAny<CancellationToken>()\n ),\n Times.Once\n );\n }\n\n [Fact]\n public async Task UnifiedAgent_ShouldCreateOptionsWithEffectiveModelNameWhenNullOptions()\n {\n // Arrange\n var config = new AppConfig\n {\n Models =\n [\n new ModelConfig\n {\n Id = \"deepseek-r1\",\n Capabilities = new ModelCapabilities\n {\n TokenLimits = new TokenLimits\n {\n MaxContextTokens = 4000,\n MaxOutputTokens = 1000,\n RecommendedMaxPromptTokens = 3000,\n },\n },\n Providers =\n [\n new ProviderConfig\n {\n Name = \"DeepSeek\",\n ModelName = \"deepseek-reasoner\", // Different from the model ID\n Priority = 1,\n Pricing = new PricingConfig { PromptPerMillion = 0.55, CompletionPerMillion = 2.19 },\n Tags = [\"reasoning\"],\n },\n ],\n },\n ],\n ProviderRegistry = new Dictionary<string, ProviderConnectionInfo>\n {\n [\"DeepSeek\"] = new ProviderConnectionInfo\n {\n EndpointUrl = \"https://api.deepseek.com/v1\",\n ApiKeyEnvironmentVariable = \"DEEPSEEK_API_KEY\",\n Compatibility = \"OpenAI\",\n },\n },\n };\n\n var services = CreateServices(config);\n\n // Create a mock model resolver that returns a predefined resolution\n var mockModelResolver = new Mock<IModelResolver>();\n var expectedResolution = new ProviderResolution\n {\n Model = config.Models[0],\n Provider = config.Models[0].Providers[0],\n Connection = config.ProviderRegistry[\"DeepSeek\"],\n };\n\n mockModelResolver\n .Setup(r =>\n r.ResolveProviderAsync(\n It.IsAny<string>(),\n It.IsAny<ProviderSelectionCriteria>(),\n It.IsAny<CancellationToken>()\n )\n )\n .ReturnsAsync(expectedResolution);\n\n // Create a mock agent factory that captures the options passed to it\n var mockAgentFactory = new Mock<IProviderAgentFactory>();\n var mockAgent = new Mock<IStreamingAgent>();\n\n GenerateReplyOptions? capturedOptions = null;\n\n mockAgent\n .Setup(a =>\n a.GenerateReplyAsync(\n It.IsAny<IEnumerable<IMessage>>(),\n It.IsAny<GenerateReplyOptions>(),\n It.IsAny<CancellationToken>()\n )\n )\n .Callback<IEnumerable<IMessage>, GenerateReplyOptions?, CancellationToken>(\n (messages, options, ct) => capturedOptions = options\n )\n .ReturnsAsync(\n [\n new TextMessage { Role = Role.Assistant, Text = \"Test response\" },\n ]\n );\n\n mockAgentFactory.Setup(f => f.CreateAgent(It.IsAny<ProviderResolution>())).Returns(mockAgent.Object);\n\n services.AddSingleton(mockModelResolver.Object);\n services.AddSingleton(mockAgentFactory.Object);\n services.AddSingleton<UnifiedAgent>();\n\n var serviceProvider = services.BuildServiceProvider();\n var unifiedAgent = serviceProvider.GetRequiredService<UnifiedAgent>();\n\n var messages = new List<IMessage>\n {\n new TextMessage { Role = Role.User, Text = \"Hello, world!\" },\n };\n\n // Pass null options but the agent should still work by getting model from context\n // We need to test this differently since we can't pass null options and expect it to work\n // Let's test with minimal options instead\n var minimalOptions = new GenerateReplyOptions { ModelId = \"deepseek-r1\" };\n\n // Act\n await unifiedAgent.GenerateReplyAsync(messages, minimalOptions);\n\n // Assert\n Assert.NotNull(capturedOptions);\n Assert.Equal(\"deepseek-reasoner\", capturedOptions.ModelId); // Should be updated to provider's model name\n\n // Verify the mock was called\n mockAgent.Verify(\n a =>\n a.GenerateReplyAsync(\n It.IsAny<IEnumerable<IMessage>>(),\n It.Is<GenerateReplyOptions>(o => o.ModelId == \"deepseek-reasoner\"),\n It.IsAny<CancellationToken>()\n ),\n Times.Once\n );\n }\n}\n"}, "988e64991217e152367177f40ce7cb270d94cb51": {"t": 1785756730.171627, "n": 20653, "h": "2ac87503efdbef2b", "c": "using AchieveAi.LmDotnetTools.LmConfig.Agents;\nusing AchieveAi.LmDotnetTools.LmConfig.Capabilities;\nusing AchieveAi.LmDotnetTools.LmConfig.Models;\nusing AchieveAi.LmDotnetTools.LmCore.Agents;\nusing AchieveAi.LmDotnetTools.LmCore.Core;\nusing AchieveAi.LmDotnetTools.LmCore.Messages;\nusing Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.Options;\nusing Moq;\n\nnamespace LmConfig.Tests;\n\npublic class UnifiedAgentTests\n{\n private static ServiceCollection CreateServices(AppConfig config)\n {\n var services = new ServiceCollection();\n services.AddLogging();\n services.AddSingleton(Options.Create(config));\n services.AddSingleton<\n AchieveAi.LmDotnetTools.LmConfig.Http.IHttpHandlerBuilder,\n AchieveAi.LmDotnetTools.LmConfig.Http.HandlerBuilder>();\n return services;\n }\n\n private static async IAsyncEnumerable<IMessage> CreateEmptyAsyncEnumerable()\n {\n await Task.CompletedTask; // Make it async\n yield break; // Return empty sequence\n }\n\n [Fact]\n public void UnifiedAgent_ShouldImplementIAgent()\n {\n // Arrange\n var config = new AppConfig\n {\n Models =\n [\n new ModelConfig\n {\n Id = \"test-model\",\n Capabilities = new ModelCapabilities\n {\n TokenLimits = new TokenLimits\n {\n MaxContextTokens = 4000,\n MaxOutputTokens = 1000,\n RecommendedMaxPromptTokens = 3000,\n },\n },\n Providers =\n [\n new ProviderConfig\n {\n Name = \"TestProvider\",\n ModelName = \"test-model-name\",\n Priority = 1,\n Pricing = new PricingConfig { PromptPerMillion = 1.0, CompletionPerMillion = 2.0 },\n Tags = [\"test\"],\n },\n ],\n },\n ],\n ProviderRegistry = new Dictionary<string, ProviderConnectionInfo>\n {\n [\"TestProvider\"] = new ProviderConnectionInfo\n {\n EndpointUrl = \"https://test.example.com\",\n ApiKeyEnvironmentVariable = \"TEST_API_KEY\",\n Compatibility = \"OpenAI\",\n },\n },\n };\n\n var services = CreateServices(config);\n services.AddSingleton<IModelResolver, ModelResolver>();\n services.AddSingleton<IProviderAgentFactory, ProviderAgentFactory>();\n services.AddSingleton<UnifiedAgent>();\n\n var serviceProvider = services.BuildServiceProvider();\n\n // Act\n var agent = serviceProvider.GetRequiredService<UnifiedAgent>();\n\n // Assert\n Assert.NotNull(agent);\n Assert.IsAssignableFrom<IAgent>(agent);\n Assert.IsAssignableFrom<IStreamingAgent>(agent);\n }\n\n [Fact]\n public async Task ModelResolver_ShouldResolveProvider()\n {\n // Arrange\n var config = new AppConfig\n {\n Models =\n [\n new ModelConfig\n {\n Id = \"test-model\",\n Capabilities = new ModelCapabilities\n {\n TokenLimits = new TokenLimits\n {\n MaxContextTokens = 4000,\n MaxOutputTokens = 1000,\n RecommendedMaxPromptTokens = 3000,\n },\n },\n Providers =\n [\n new ProviderConfig\n {\n Name = \"TestProvider\",\n ModelName = \"test-model-name\",\n Priority = 1,\n Pricing = new PricingConfig { PromptPerMillion = 1.0, CompletionPerMillion = 2.0 },\n Tags = [\"test\"],\n },\n ],\n },\n ],\n ProviderRegistry = new Dictionary<string, ProviderConnectionInfo>\n {\n [\"TestProvider\"] = new ProviderConnectionInfo\n {\n EndpointUrl = \"https://test.example.com\",\n ApiKeyEnvironmentVariable = \"TEST_API_KEY\",\n Compatibility = \"OpenAI\",\n },\n },\n };\n\n // Set up the required environment variable for the test\n Environment.SetEnvironmentVariable(\"TEST_API_KEY\", \"test-api-key\");\n\n try\n {\n var services = CreateServices(config);\n services.AddSingleton<IModelResolver, ModelResolver>();\n\n var serviceProvider = services.BuildServiceProvider();\n var resolver = serviceProvider.GetRequiredService<IModelResolver>();\n\n // Act\n var resolution = await resolver.ResolveProviderAsync(\"test-model\");\n\n // Assert\n Assert.NotNull(resolution);\n Assert.Equal(\"test-model\", resolution.Model.Id);\n Assert.Equal(\"TestProvider\", resolution.Provider.Name);\n Assert.Equal(\"test-model-name\", resolution.Provider.ModelName);\n Assert.NotNull(resolution.Connection);\n Assert.Equal(\"https://test.example.com\", resolution.Connection.EndpointUrl);\n }\n finally\n {\n // Clean up the environment variable\n Environment.SetEnvironmentVariable(\"TEST_API_KEY\", null);\n }\n }\n\n [Fact]\n public async Task ModelResolver_ShouldReturnNullForUnknownModel()\n {\n // Arrange\n var config = new AppConfig\n {\n Models = [],\n ProviderRegistry = new Dictionary<string, ProviderConnectionInfo>(),\n };\n\n var services = CreateServices(config);\n services.AddSingleton<IModelResolver, ModelResolver>();\n\n var serviceProvider = services.BuildServiceProvider();\n var resolver = serviceProvider.GetRequiredService<IModelResolver>();\n\n // Act\n var resolution = await resolver.ResolveProviderAsync(\"unknown-model\");\n\n // Assert\n Assert.Null(resolution);\n }\n\n [Fact]\n public async Task UnifiedAgent_ShouldUpdateModelIdWhenForwardingToProviderAgent()\n {\n // Arrange\n var config = new AppConfig\n {\n Models =\n [\n new ModelConfig\n {\n Id = \"gpt-4.1\",\n Capabilities = new ModelCapabilities\n {\n TokenLimits = new TokenLimits\n {\n MaxContextTokens = 4000,\n MaxOutputTokens = 1000,\n RecommendedMaxPromptTokens = 3000,\n },\n },\n Providers =\n [\n new ProviderConfig\n {\n Name = \"OpenRouter\",\n ModelName = \"openai/gpt-4.1\", // Different from the model ID\n Priority = 1,\n Pricing = new PricingConfig { PromptPerMillion = 2.0, CompletionPerMillion = 8.0 },\n Tags = [\"openai-compatible\"],\n },\n ],\n },\n ],\n ProviderRegistry = new Dictionary<string, ProviderConnectionInfo>\n {\n [\"OpenRouter\"] = new ProviderConnectionInfo\n {\n EndpointUrl = \"https://openrouter.ai/api/v1\",\n ApiKeyEnvironmentVariable = \"OPENROUTER_API_KEY\",\n Compatibility = \"OpenAI\",\n },\n },\n };\n\n var services = CreateServices(config);\n\n // Create a mock model resolver that returns a predefined resolution\n var mockModelResolver = new Mock<IModelResolver>();\n var expectedResolution = new ProviderResolution\n {\n Model = config.Models[0],\n Provider = config.Models[0].Providers[0],\n Connection = config.ProviderRegistry[\"OpenRouter\"],\n };\n\n mockModelResolver\n .Setup(r =>\n r.ResolveProviderAsync(\n It.IsAny<string>(),\n It.IsAny<ProviderSelectionCriteria>(),\n It.IsAny<CancellationToken>()\n )\n )\n .ReturnsAsync(expectedResolution);\n\n // Create a mock agent factory that captures the options passed to it\n var mockAgentFactory = new Mock<IProviderAgentFactory>();\n var mockAgent = new Mock<IStreamingAgent>();\n\n GenerateReplyOptions? capturedOptions = null;\n\n mockAgent\n .Setup(a =>\n a.GenerateReplyAsync(\n It.IsAny<IEnumerable<IMessage>>(),\n It.IsAny<GenerateReplyOptions>(),\n It.IsAny<CancellationToken>()\n )\n )\n .Callback<IEnumerable<IMessage>, GenerateReplyOptions?, CancellationToken>(\n (messages, options, ct) => capturedOptions = options\n )\n .ReturnsAsync(\n [\n new TextMessage { Role = Role.Assistant, Text = \"Test response\" },\n ]\n );\n\n mockAgentFactory.Setup(f => f.CreateAgent(It.IsAny<ProviderResolution>())).Returns(mockAgent.Object);\n\n services.AddSingleton(mockModelResolver.Object);\n services.AddSingleton(mockAgentFactory.Object);\n services.AddSingleton<UnifiedAgent>();\n\n var serviceProvider = services.BuildServiceProvider();\n var unifiedAgent = serviceProvider.GetRequiredService<UnifiedAgent>();\n\n var messages = new List<IMessage>\n {\n new TextMessage { Role = Role.User, Text = \"Hello, world!\" },\n };\n\n var originalOptions = new GenerateReplyOptions\n {\n ModelId = \"gpt-4.1\", // User specifies the logical model ID\n Temperature = 0.7f,\n };\n\n // Act\n await unifiedAgent.GenerateReplyAsync(messages, originalOptions);\n\n // Assert\n Assert.NotNull(capturedOptions);\n Assert.Equal(\"openai/gpt-4.1\", capturedOptions.ModelId); // Should be updated to provider's model name\n Assert.Equal(0.7f, capturedOptions.Temperature); // Other properties should be preserved\n\n // Verify the mock was called\n mockAgent.Verify(\n a =>\n a.GenerateReplyAsync(\n It.IsAny<IEnumerable<IMessage>>(),\n It.Is<GenerateReplyOptions>(o => o.ModelId == \"openai/gpt-4.1\"),\n It.IsAny<CancellationToken>()\n ),\n Times.Once\n );\n }\n\n [Fact]\n public async Task UnifiedAgent_ShouldUpdateModelIdForStreamingRequests()\n {\n // Arrange\n var config = new AppConfig\n {\n Models =\n [\n new ModelConfig\n {\n Id = \"claude-3-sonnet\",\n Capabilities = new ModelCapabilities\n {\n TokenLimits = new TokenLimits\n {\n MaxContextTokens = 4000,\n MaxOutputTokens = 1000,\n RecommendedMaxPromptTokens = 3000,\n },\n },\n Providers =\n [\n new ProviderConfig\n {\n Name = \"OpenRouter\",\n ModelName = \"anthropic/claude-3-sonnet\", // Different from the model ID\n Priority = 1,\n Pricing = new PricingConfig { PromptPerMillion = 3.0, CompletionPerMillion = 15.0 },\n Tags = [\"openai-compatible\"],\n },\n ],\n },\n ],\n ProviderRegistry = new Dictionary<string, ProviderConnectionInfo>\n {\n [\"OpenRouter\"] = new ProviderConnectionInfo\n {\n EndpointUrl = \"https://openrouter.ai/api/v1\",\n ApiKeyEnvironmentVariable = \"OPENROUTER_API_KEY\",\n Compatibility = \"OpenAI\",\n },\n },\n };\n\n var services = CreateServices(config);\n\n // Create a mock model resolver that returns a predefined resolution\n var mockModelResolver = new Mock<IModelResolver>();\n var expectedResolution = new ProviderResolution\n {\n Model = config.Models[0],\n Provider = config.Models[0].Providers[0],\n Connection = config.ProviderRegistry[\"OpenRouter\"],\n };\n\n mockModelResolver\n .Setup(r =>\n r.ResolveProviderAsync(\n It.IsAny<string>(),\n It.IsAny<ProviderSelectionCriteria>(),\n It.IsAny<CancellationToken>()\n )\n )\n .ReturnsAsync(expectedResolution);\n\n // Create a mock agent factory that captures the options passed to it\n var mockAgentFactory = new Mock<IProviderAgentFactory>();\n var mockAgent = new Mock<IStreamingAgent>();\n\n GenerateReplyOptions? capturedOptions = null;\n\n mockAgent\n .Setup(a =>\n a.GenerateReplyStreamingAsync(\n It.IsAny<IEnumerable<IMessage>>(),\n It.IsAny<GenerateReplyOptions>(),\n It.IsAny<CancellationToken>()\n )\n )\n .Callback<IEnumerable<IMessage>, GenerateReplyOptions?, CancellationToken>(\n (messages, options, ct) => capturedOptions = options\n )\n .ReturnsAsync(CreateEmptyAsyncEnumerable());\n\n mockAgentFactory.Setup(f => f.CreateStreamingAgent(It.IsAny<ProviderResolution>())).Returns(mockAgent.Object);\n\n services.AddSingleton(mockModelResolver.Object);\n services.AddSingleton(mockAgentFactory.Object);\n services.AddSingleton<UnifiedAgent>();\n\n var serviceProvider = services.BuildServiceProvider();\n var unifiedAgent = serviceProvider.GetRequiredService<UnifiedAgent>();\n\n var messages = new List<IMessage>\n {\n new TextMessage { Role = Role.User, Text = \"Hello, world!\" },\n };\n\n var originalOptions = new GenerateReplyOptions\n {\n ModelId = \"claude-3-sonnet\", // User specifies the logical model ID\n Temperature = 0.5f,\n MaxToken = 2000,\n };\n\n // Act\n await unifiedAgent.GenerateReplyStreamingAsync(messages, originalOptions);\n\n // Assert\n Assert.NotNull(capturedOptions);\n Assert.Equal(\"anthropic/claude-3-sonnet\", capturedOptions.ModelId); // Should be updated to provider's model name\n Assert.Equal(0.5f, capturedOptions.Temperature); // Other properties should be preserved\n Assert.Equal(2000, capturedOptions.MaxToken); // Other properties should be preserved\n\n // Verify the mock was called\n mockAgent.Verify(\n a =>\n a.GenerateReplyStreamingAsync(\n It.IsAny<IEnumerable<IMessage>>(),\n It.Is<GenerateReplyOptions>(o => o.ModelId == \"anthropic/claude-3-sonnet\"),\n It.IsAny<CancellationToken>()\n ),\n Times.Once\n );\n }\n\n [Fact]\n public async Task UnifiedAgent_ShouldCreateOptionsWithEffectiveModelNameWhenNullOptions()\n {\n // Arrange\n var config = new AppConfig\n {\n Models =\n [\n new ModelConfig\n {\n Id = \"deepseek-r1\",\n Capabilities = new ModelCapabilities\n {\n TokenLimits = new TokenLimits\n {\n MaxContextTokens = 4000,\n MaxOutputTokens = 1000,\n RecommendedMaxPromptTokens = 3000,\n },\n },\n Providers =\n [\n new ProviderConfig\n {\n Name = \"DeepSeek\",\n ModelName = \"deepseek-reasoner\", // Different from the model ID\n Priority = 1,\n Pricing = new PricingConfig { PromptPerMillion = 0.55, CompletionPerMillion = 2.19 },\n Tags = [\"reasoning\"],\n },\n ],\n },\n ],\n ProviderRegistry = new Dictionary<string, ProviderConnectionInfo>\n {\n [\"DeepSeek\"] = new ProviderConnectionInfo\n {\n EndpointUrl = \"https://api.deepseek.com/v1\",\n ApiKeyEnvironmentVariable = \"DEEPSEEK_API_KEY\",\n Compatibility = \"OpenAI\",\n },\n },\n };\n\n var services = CreateServices(config);\n\n // Create a mock model resolver that returns a predefined resolution\n var mockModelResolver = new Mock<IModelResolver>();\n var expectedResolution = new ProviderResolution\n {\n Model = config.Models[0],\n Provider = config.Models[0].Providers[0],\n Connection = config.ProviderRegistry[\"DeepSeek\"],\n };\n\n mockModelResolver\n .Setup(r =>\n r.ResolveProviderAsync(\n It.IsAny<string>(),\n It.IsAny<ProviderSelectionCriteria>(),\n It.IsAny<CancellationToken>()\n )\n )\n .ReturnsAsync(expectedResolution);\n\n // Create a mock agent factory that captures the options passed to it\n var mockAgentFactory = new Mock<IProviderAgentFactory>();\n var mockAgent = new Mock<IStreamingAgent>();\n\n GenerateReplyOptions? capturedOptions = null;\n\n mockAgent\n .Setup(a =>\n a.GenerateReplyAsync(\n It.IsAny<IEnumerable<IMessage>>(),\n It.IsAny<GenerateReplyOptions>(),\n It.IsAny<CancellationToken>()\n )\n )\n .Callback<IEnumerable<IMessage>, GenerateReplyOptions?, CancellationToken>(\n (messages, options, ct) => capturedOptions = options\n )\n .ReturnsAsync(\n [\n new TextMessage { Role = Role.Assistant, Text = \"Test response\" },\n ]\n );\n\n mockAgentFactory.Setup(f => f.CreateAgent(It.IsAny<ProviderResolution>())).Returns(mockAgent.Object);\n\n services.AddSingleton(mockModelResolver.Object);\n services.AddSingleton(mockAgentFactory.Object);\n services.AddSingleton<UnifiedAgent>();\n\n var serviceProvider = services.BuildServiceProvider();\n var unifiedAgent = serviceProvider.GetRequiredService<UnifiedAgent>();\n\n var messages = new List<IMessage>\n {\n new TextMessage { Role = Role.User, Text = \"Hello, world!\" },\n };\n\n // Pass null options but the agent should still work by getting model from context\n // We need to test this differently since we can't pass null options and expect it to work\n // Let's test with minimal options instead\n var minimalOptions = new GenerateReplyOptions { ModelId = \"deepseek-r1\" };\n\n // Act\n await unifiedAgent.GenerateReplyAsync(messages, minimalOptions);\n\n // Assert\n Assert.NotNull(capturedOptions);\n Assert.Equal(\"deepseek-reasoner\", capturedOptions.ModelId); // Should be updated to provider's model name\n\n // Verify the mock was called\n mockAgent.Verify(\n a =>\n a.GenerateReplyAsync(\n It.IsAny<IEnumerable<IMessage>>(),\n It.Is<GenerateReplyOptions>(o => o.ModelId == \"deepseek-reasoner\"),\n It.IsAny<CancellationToken>()\n ),\n Times.Once\n );\n }\n}\n"}, "0646d27bed0ef761c17b4275c9662aaf3d3f271c": {"t": 1785756731.4511137, "n": 20915, "h": "22989f0bc6020378", "c": "using System.Linq;\nusing AchieveAi.LmDotnetTools.LmConfig.Agents;\nusing AchieveAi.LmDotnetTools.LmConfig.Capabilities;\nusing AchieveAi.LmDotnetTools.LmConfig.Models;\nusing AchieveAi.LmDotnetTools.LmCore.Agents;\nusing AchieveAi.LmDotnetTools.LmCore.Core;\nusing AchieveAi.LmDotnetTools.LmCore.Messages;\nusing Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.Logging;\nusing Microsoft.Extensions.Options;\nusing Moq;\nusing Xunit;\n\nnamespace LmConfig.Tests;\n\npublic class UnifiedAgentTests\n{\n private static ServiceCollection CreateServices(AppConfig config)\n {\n var services = new ServiceCollection();\n services.AddLogging();\n services.AddSingleton(Options.Create(config));\n services.AddSingleton<\n AchieveAi.LmDotnetTools.LmConfig.Http.IHttpHandlerBuilder,\n AchieveAi.LmDotnetTools.LmConfig.Http.HandlerBuilder>();\n return services;\n }\n\n private static async IAsyncEnumerable<IMessage> CreateEmptyAsyncEnumerable()\n {\n await Task.CompletedTask; // Make it async\n yield break; // Return empty sequence\n }\n\n [Fact]\n public void UnifiedAgent_ShouldImplementIAgent()\n {\n // Arrange\n var config = new AppConfig\n {\n Models = new[]\n {\n new ModelConfig\n {\n Id = \"test-model\",\n Capabilities = new ModelCapabilities\n {\n TokenLimits = new TokenLimits\n {\n MaxContextTokens = 4000,\n MaxOutputTokens = 1000,\n RecommendedMaxPromptTokens = 3000,\n },\n },\n Providers = new[]\n {\n new ProviderConfig\n {\n Name = \"TestProvider\",\n ModelName = \"test-model-name\",\n Priority = 1,\n Pricing = new PricingConfig { PromptPerMillion = 1.0, CompletionPerMillion = 2.0 },\n Tags = new[] { \"test\" },\n },\n },\n },\n },\n ProviderRegistry = new Dictionary<string, ProviderConnectionInfo>\n {\n [\"TestProvider\"] = new ProviderConnectionInfo\n {\n EndpointUrl = \"https://test.example.com\",\n ApiKeyEnvironmentVariable = \"TEST_API_KEY\",\n Compatibility = \"OpenAI\",\n },\n },\n };\n\n var services = CreateServices(config);\n services.AddSingleton<IModelResolver, ModelResolver>();\n services.AddSingleton<IProviderAgentFactory, ProviderAgentFactory>();\n services.AddSingleton<UnifiedAgent>();\n\n var serviceProvider = services.BuildServiceProvider();\n\n // Act\n var agent = serviceProvider.GetRequiredService<UnifiedAgent>();\n\n // Assert\n Assert.NotNull(agent);\n Assert.IsAssignableFrom<IAgent>(agent);\n Assert.IsAssignableFrom<IStreamingAgent>(agent);\n }\n\n [Fact]\n public async Task ModelResolver_ShouldResolveProvider()\n {\n // Arrange\n var config = new AppConfig\n {\n Models = new[]\n {\n new ModelConfig\n {\n Id = \"test-model\",\n Capabilities = new ModelCapabilities\n {\n TokenLimits = new TokenLimits\n {\n MaxContextTokens = 4000,\n MaxOutputTokens = 1000,\n RecommendedMaxPromptTokens = 3000,\n },\n },\n Providers = new[]\n {\n new ProviderConfig\n {\n Name = \"TestProvider\",\n ModelName = \"test-model-name\",\n Priority = 1,\n Pricing = new PricingConfig { PromptPerMillion = 1.0, CompletionPerMillion = 2.0 },\n Tags = new[] { \"test\" },\n },\n },\n },\n },\n ProviderRegistry = new Dictionary<string, ProviderConnectionInfo>\n {\n [\"TestProvider\"] = new ProviderConnectionInfo\n {\n EndpointUrl = \"https://test.example.com\",\n ApiKeyEnvironmentVariable = \"TEST_API_KEY\",\n Compatibility = \"OpenAI\",\n },\n },\n };\n\n // Set up the required environment variable for the test\n Environment.SetEnvironmentVariable(\"TEST_API_KEY\", \"test-api-key\");\n\n try\n {\n var services = CreateServices(config);\n services.AddSingleton<IModelResolver, ModelResolver>();\n\n var serviceProvider = services.BuildServiceProvider();\n var resolver = serviceProvider.GetRequiredService<IModelResolver>();\n\n // Act\n var resolution = await resolver.ResolveProviderAsync(\"test-model\");\n\n // Assert\n Assert.NotNull(resolution);\n Assert.Equal(\"test-model\", resolution.Model.Id);\n Assert.Equal(\"TestProvider\", resolution.Provider.Name);\n Assert.Equal(\"test-model-name\", resolution.Provider.ModelName);\n Assert.NotNull(resolution.Connection);\n Assert.Equal(\"https://test.example.com\", resolution.Connection.EndpointUrl);\n }\n finally\n {\n // Clean up the environment variable\n Environment.SetEnvironmentVariable(\"TEST_API_KEY\", null);\n }\n }\n\n [Fact]\n public async Task ModelResolver_ShouldReturnNullForUnknownModel()\n {\n // Arrange\n var config = new AppConfig\n {\n Models = Array.Empty<ModelConfig>(),\n ProviderRegistry = new Dictionary<string, ProviderConnectionInfo>(),\n };\n\n var services = CreateServices(config);\n services.AddSingleton<IModelResolver, ModelResolver>();\n\n var serviceProvider = services.BuildServiceProvider();\n var resolver = serviceProvider.GetRequiredService<IModelResolver>();\n\n // Act\n var resolution = await resolver.ResolveProviderAsync(\"unknown-model\");\n\n // Assert\n Assert.Null(resolution);\n }\n\n [Fact]\n public async Task UnifiedAgent_ShouldUpdateModelIdWhenForwardingToProviderAgent()\n {\n // Arrange\n var config = new AppConfig\n {\n Models = new[]\n {\n new ModelConfig\n {\n Id = \"gpt-4.1\",\n Capabilities = new ModelCapabilities\n {\n TokenLimits = new TokenLimits\n {\n MaxContextTokens = 4000,\n MaxOutputTokens = 1000,\n RecommendedMaxPromptTokens = 3000,\n },\n },\n Providers = new[]\n {\n new ProviderConfig\n {\n Name = \"OpenRouter\",\n ModelName = \"openai/gpt-4.1\", // Different from the model ID\n Priority = 1,\n Pricing = new PricingConfig { PromptPerMillion = 2.0, CompletionPerMillion = 8.0 },\n Tags = new[] { \"openai-compatible\" },\n },\n },\n },\n },\n ProviderRegistry = new Dictionary<string, ProviderConnectionInfo>\n {\n [\"OpenRouter\"] = new ProviderConnectionInfo\n {\n EndpointUrl = \"https://openrouter.ai/api/v1\",\n ApiKeyEnvironmentVariable = \"OPENROUTER_API_KEY\",\n Compatibility = \"OpenAI\",\n },\n },\n };\n\n var services = CreateServices(config);\n\n // Create a mock model resolver that returns a predefined resolution\n var mockModelResolver = new Mock<IModelResolver>();\n var expectedResolution = new ProviderResolution\n {\n Model = config.Models[0],\n Provider = config.Models[0].Providers[0],\n Connection = config.ProviderRegistry[\"OpenRouter\"],\n };\n\n mockModelResolver\n .Setup(r =>\n r.ResolveProviderAsync(\n It.IsAny<string>(),\n It.IsAny<ProviderSelectionCriteria>(),\n It.IsAny<CancellationToken>()\n )\n )\n .ReturnsAsync(expectedResolution);\n\n // Create a mock agent factory that captures the options passed to it\n var mockAgentFactory = new Mock<IProviderAgentFactory>();\n var mockAgent = new Mock<IStreamingAgent>();\n\n GenerateReplyOptions? capturedOptions = null;\n\n mockAgent\n .Setup(a =>\n a.GenerateReplyAsync(\n It.IsAny<IEnumerable<IMessage>>(),\n It.IsAny<GenerateReplyOptions>(),\n It.IsAny<CancellationToken>()\n )\n )\n .Callback<IEnumerable<IMessage>, GenerateReplyOptions?, CancellationToken>(\n (messages, options, ct) => capturedOptions = options\n )\n .ReturnsAsync(\n new List<IMessage>\n {\n new TextMessage { Role = Role.Assistant, Text = \"Test response\" },\n }\n );\n\n mockAgentFactory.Setup(f => f.CreateAgent(It.IsAny<ProviderResolution>())).Returns(mockAgent.Object);\n\n services.AddSingleton(mockModelResolver.Object);\n services.AddSingleton(mockAgentFactory.Object);\n services.AddSingleton<UnifiedAgent>();\n\n var serviceProvider = services.BuildServiceProvider();\n var unifiedAgent = serviceProvider.GetRequiredService<UnifiedAgent>();\n\n var messages = new List<IMessage>\n {\n new TextMessage { Role = Role.User, Text = \"Hello, world!\" },\n };\n\n var originalOptions = new GenerateReplyOptions\n {\n ModelId = \"gpt-4.1\", // User specifies the logical model ID\n Temperature = 0.7f,\n };\n\n // Act\n await unifiedAgent.GenerateReplyAsync(messages, originalOptions);\n\n // Assert\n Assert.NotNull(capturedOptions);\n Assert.Equal(\"openai/gpt-4.1\", capturedOptions.ModelId); // Should be updated to provider's model name\n Assert.Equal(0.7f, capturedOptions.Temperature); // Other properties should be preserved\n\n // Verify the mock was called\n mockAgent.Verify(\n a =>\n a.GenerateReplyAsync(\n It.IsAny<IEnumerable<IMessage>>(),\n It.Is<GenerateReplyOptions>(o => o.ModelId == \"openai/gpt-4.1\"),\n It.IsAny<CancellationToken>()\n ),\n Times.Once\n );\n }\n\n [Fact]\n public async Task UnifiedAgent_ShouldUpdateModelIdForStreamingRequests()\n {\n // Arrange\n var config = new AppConfig\n {\n Models = new[]\n {\n new ModelConfig\n {\n Id = \"claude-3-sonnet\",\n Capabilities = new ModelCapabilities\n {\n TokenLimits = new TokenLimits\n {\n MaxContextTokens = 4000,\n MaxOutputTokens = 1000,\n RecommendedMaxPromptTokens = 3000,\n },\n },\n Providers = new[]\n {\n new ProviderConfig\n {\n Name = \"OpenRouter\",\n ModelName = \"anthropic/claude-3-sonnet\", // Different from the model ID\n Priority = 1,\n Pricing = new PricingConfig { PromptPerMillion = 3.0, CompletionPerMillion = 15.0 },\n Tags = new[] { \"openai-compatible\" },\n },\n },\n },\n },\n ProviderRegistry = new Dictionary<string, ProviderConnectionInfo>\n {\n [\"OpenRouter\"] = new ProviderConnectionInfo\n {\n EndpointUrl = \"https://openrouter.ai/api/v1\",\n ApiKeyEnvironmentVariable = \"OPENROUTER_API_KEY\",\n Compatibility = \"OpenAI\",\n },\n },\n };\n\n var services = CreateServices(config);\n\n // Create a mock model resolver that returns a predefined resolution\n var mockModelResolver = new Mock<IModelResolver>();\n var expectedResolution = new ProviderResolution\n {\n Model = config.Models[0],\n Provider = config.Models[0].Providers[0],\n Connection = config.ProviderRegistry[\"OpenRouter\"],\n };\n\n mockModelResolver\n .Setup(r =>\n r.ResolveProviderAsync(\n It.IsAny<string>(),\n It.IsAny<ProviderSelectionCriteria>(),\n It.IsAny<CancellationToken>()\n )\n )\n .ReturnsAsync(expectedResolution);\n\n // Create a mock agent factory that captures the options passed to it\n var mockAgentFactory = new Mock<IProviderAgentFactory>();\n var mockAgent = new Mock<IStreamingAgent>();\n\n GenerateReplyOptions? capturedOptions = null;\n\n mockAgent\n .Setup(a =>\n a.GenerateReplyStreamingAsync(\n It.IsAny<IEnumerable<IMessage>>(),\n It.IsAny<GenerateReplyOptions>(),\n It.IsAny<CancellationToken>()\n )\n )\n .Callback<IEnumerable<IMessage>, GenerateReplyOptions?, CancellationToken>(\n (messages, options, ct) => capturedOptions = options\n )\n .ReturnsAsync(CreateEmptyAsyncEnumerable());\n\n mockAgentFactory.Setup(f => f.CreateStreamingAgent(It.IsAny<ProviderResolution>())).Returns(mockAgent.Object);\n\n services.AddSingleton(mockModelResolver.Object);\n services.AddSingleton(mockAgentFactory.Object);\n services.AddSingleton<UnifiedAgent>();\n\n var serviceProvider = services.BuildServiceProvider();\n var unifiedAgent = serviceProvider.GetRequiredService<UnifiedAgent>();\n\n var messages = new List<IMessage>\n {\n new TextMessage { Role = Role.User, Text = \"Hello, world!\" },\n };\n\n var originalOptions = new GenerateReplyOptions\n {\n ModelId = \"claude-3-sonnet\", // User specifies the logical model ID\n Temperature = 0.5f,\n MaxToken = 2000,\n };\n\n // Act\n await unifiedAgent.GenerateReplyStreamingAsync(messages, originalOptions);\n\n // Assert\n Assert.NotNull(capturedOptions);\n Assert.Equal(\"anthropic/claude-3-sonnet\", capturedOptions.ModelId); // Should be updated to provider's model name\n Assert.Equal(0.5f, capturedOptions.Temperature); // Other properties should be preserved\n Assert.Equal(2000, capturedOptions.MaxToken); // Other properties should be preserved\n\n // Verify the mock was called\n mockAgent.Verify(\n a =>\n a.GenerateReplyStreamingAsync(\n It.IsAny<IEnumerable<IMessage>>(),\n It.Is<GenerateReplyOptions>(o => o.ModelId == \"anthropic/claude-3-sonnet\"),\n It.IsAny<CancellationToken>()\n ),\n Times.Once\n );\n }\n\n [Fact]\n public async Task UnifiedAgent_ShouldCreateOptionsWithEffectiveModelNameWhenNullOptions()\n {\n // Arrange\n var config = new AppConfig\n {\n Models = new[]\n {\n new ModelConfig\n {\n Id = \"deepseek-r1\",\n Capabilities = new ModelCapabilities\n {\n TokenLimits = new TokenLimits\n {\n MaxContextTokens = 4000,\n MaxOutputTokens = 1000,\n RecommendedMaxPromptTokens = 3000,\n },\n },\n Providers = new[]\n {\n new ProviderConfig\n {\n Name = \"DeepSeek\",\n ModelName = \"deepseek-reasoner\", // Different from the model ID\n Priority = 1,\n Pricing = new PricingConfig { PromptPerMillion = 0.55, CompletionPerMillion = 2.19 },\n Tags = new[] { \"reasoning\" },\n },\n },\n },\n },\n ProviderRegistry = new Dictionary<string, ProviderConnectionInfo>\n {\n [\"DeepSeek\"] = new ProviderConnectionInfo\n {\n EndpointUrl = \"https://api.deepseek.com/v1\",\n ApiKeyEnvironmentVariable = \"DEEPSEEK_API_KEY\",\n Compatibility = \"OpenAI\",\n },\n },\n };\n\n var services = CreateServices(config);\n\n // Create a mock model resolver that returns a predefined resolution\n var mockModelResolver = new Mock<IModelResolver>();\n var expectedResolution = new ProviderResolution\n {\n Model = config.Models[0],\n Provider = config.Models[0].Providers[0],\n Connection = config.ProviderRegistry[\"DeepSeek\"],\n };\n\n mockModelResolver\n .Setup(r =>\n r.ResolveProviderAsync(\n It.IsAny<string>(),\n It.IsAny<ProviderSelectionCriteria>(),\n It.IsAny<CancellationToken>()\n )\n )\n .ReturnsAsync(expectedResolution);\n\n // Create a mock agent factory that captures the options passed to it\n var mockAgentFactory = new Mock<IProviderAgentFactory>();\n var mockAgent = new Mock<IStreamingAgent>();\n\n GenerateReplyOptions? capturedOptions = null;\n\n mockAgent\n .Setup(a =>\n a.GenerateReplyAsync(\n It.IsAny<IEnumerable<IMessage>>(),\n It.IsAny<GenerateReplyOptions>(),\n It.IsAny<CancellationToken>()\n )\n )\n .Callback<IEnumerable<IMessage>, GenerateReplyOptions?, CancellationToken>(\n (messages, options, ct) => capturedOptions = options\n )\n .ReturnsAsync(\n new List<IMessage>\n {\n new TextMessage { Role = Role.Assistant, Text = \"Test response\" },\n }\n );\n\n mockAgentFactory.Setup(f => f.CreateAgent(It.IsAny<ProviderResolution>())).Returns(mockAgent.Object);\n\n services.AddSingleton(mockModelResolver.Object);\n services.AddSingleton(mockAgentFactory.Object);\n services.AddSingleton<UnifiedAgent>();\n\n var serviceProvider = services.BuildServiceProvider();\n var unifiedAgent = serviceProvider.GetRequiredService<UnifiedAgent>();\n\n var messages = new List<IMessage>\n {\n new TextMessage { Role = Role.User, Text = \"Hello, world!\" },\n };\n\n // Pass null options but the agent should still work by getting model from context\n // We need to test this differently since we can't pass null options and expect it to work\n // Let's test with minimal options instead\n var minimalOptions = new GenerateReplyOptions { ModelId = \"deepseek-r1\" };\n\n // Act\n await unifiedAgent.GenerateReplyAsync(messages, minimalOptions);\n\n // Assert\n Assert.NotNull(capturedOptions);\n Assert.Equal(\"deepseek-reasoner\", capturedOptions.ModelId); // Should be updated to provider's model name\n\n // Verify the mock was called\n mockAgent.Verify(\n a =>\n a.GenerateReplyAsync(\n It.IsAny<IEnumerable<IMessage>>(),\n It.Is<GenerateReplyOptions>(o => o.ModelId == \"deepseek-reasoner\"),\n It.IsAny<CancellationToken>()\n ),\n Times.Once\n );\n }\n}\n"}, "762155ee02546d230e93f80853f42eaf287ba779": {"t": 1785756733.1412783, "n": 20744, "h": "ad702d83ebae2239", "c": "using System.Linq;\nusing AchieveAi.LmDotnetTools.LmConfig.Agents;\nusing AchieveAi.LmDotnetTools.LmConfig.Capabilities;\nusing AchieveAi.LmDotnetTools.LmConfig.Models;\nusing AchieveAi.LmDotnetTools.LmCore.Agents;\nusing AchieveAi.LmDotnetTools.LmCore.Core;\nusing AchieveAi.LmDotnetTools.LmCore.Messages;\nusing Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.Logging;\nusing Microsoft.Extensions.Options;\nusing Moq;\nusing Xunit;\n\nnamespace LmConfig.Tests;\n\npublic class UnifiedAgentTests\n{\n private static ServiceCollection CreateServices(AppConfig config)\n {\n var services = new ServiceCollection();\n services.AddLogging();\n services.AddSingleton(Options.Create(config));\n return services;\n }\n\n private static async IAsyncEnumerable<IMessage> CreateEmptyAsyncEnumerable()\n {\n await Task.CompletedTask; // Make it async\n yield break; // Return empty sequence\n }\n\n [Fact]\n public void UnifiedAgent_ShouldImplementIAgent()\n {\n // Arrange\n var config = new AppConfig\n {\n Models = new[]\n {\n new ModelConfig\n {\n Id = \"test-model\",\n Capabilities = new ModelCapabilities\n {\n TokenLimits = new TokenLimits\n {\n MaxContextTokens = 4000,\n MaxOutputTokens = 1000,\n RecommendedMaxPromptTokens = 3000,\n },\n },\n Providers = new[]\n {\n new ProviderConfig\n {\n Name = \"TestProvider\",\n ModelName = \"test-model-name\",\n Priority = 1,\n Pricing = new PricingConfig { PromptPerMillion = 1.0, CompletionPerMillion = 2.0 },\n Tags = new[] { \"test\" },\n },\n },\n },\n },\n ProviderRegistry = new Dictionary<string, ProviderConnectionInfo>\n {\n [\"TestProvider\"] = new ProviderConnectionInfo\n {\n EndpointUrl = \"https://test.example.com\",\n ApiKeyEnvironmentVariable = \"TEST_API_KEY\",\n Compatibility = \"OpenAI\",\n },\n },\n };\n\n var services = CreateServices(config);\n services.AddSingleton<IModelResolver, ModelResolver>();\n services.AddSingleton<IProviderAgentFactory, ProviderAgentFactory>();\n services.AddSingleton<UnifiedAgent>();\n\n var serviceProvider = services.BuildServiceProvider();\n\n // Act\n var agent = serviceProvider.GetRequiredService<UnifiedAgent>();\n\n // Assert\n Assert.NotNull(agent);\n Assert.IsAssignableFrom<IAgent>(agent);\n Assert.IsAssignableFrom<IStreamingAgent>(agent);\n }\n\n [Fact]\n public async Task ModelResolver_ShouldResolveProvider()\n {\n // Arrange\n var config = new AppConfig\n {\n Models = new[]\n {\n new ModelConfig\n {\n Id = \"test-model\",\n Capabilities = new ModelCapabilities\n {\n TokenLimits = new TokenLimits\n {\n MaxContextTokens = 4000,\n MaxOutputTokens = 1000,\n RecommendedMaxPromptTokens = 3000,\n },\n },\n Providers = new[]\n {\n new ProviderConfig\n {\n Name = \"TestProvider\",\n ModelName = \"test-model-name\",\n Priority = 1,\n Pricing = new PricingConfig { PromptPerMillion = 1.0, CompletionPerMillion = 2.0 },\n Tags = new[] { \"test\" },\n },\n },\n },\n },\n ProviderRegistry = new Dictionary<string, ProviderConnectionInfo>\n {\n [\"TestProvider\"] = new ProviderConnectionInfo\n {\n EndpointUrl = \"https://test.example.com\",\n ApiKeyEnvironmentVariable = \"TEST_API_KEY\",\n Compatibility = \"OpenAI\",\n },\n },\n };\n\n // Set up the required environment variable for the test\n Environment.SetEnvironmentVariable(\"TEST_API_KEY\", \"test-api-key\");\n\n try\n {\n var services = CreateServices(config);\n services.AddSingleton<IModelResolver, ModelResolver>();\n\n var serviceProvider = services.BuildServiceProvider();\n var resolver = serviceProvider.GetRequiredService<IModelResolver>();\n\n // Act\n var resolution = await resolver.ResolveProviderAsync(\"test-model\");\n\n // Assert\n Assert.NotNull(resolution);\n Assert.Equal(\"test-model\", resolution.Model.Id);\n Assert.Equal(\"TestProvider\", resolution.Provider.Name);\n Assert.Equal(\"test-model-name\", resolution.Provider.ModelName);\n Assert.NotNull(resolution.Connection);\n Assert.Equal(\"https://test.example.com\", resolution.Connection.EndpointUrl);\n }\n finally\n {\n // Clean up the environment variable\n Environment.SetEnvironmentVariable(\"TEST_API_KEY\", null);\n }\n }\n\n [Fact]\n public async Task ModelResolver_ShouldReturnNullForUnknownModel()\n {\n // Arrange\n var config = new AppConfig\n {\n Models = Array.Empty<ModelConfig>(),\n ProviderRegistry = new Dictionary<string, ProviderConnectionInfo>(),\n };\n\n var services = CreateServices(config);\n services.AddSingleton<IModelResolver, ModelResolver>();\n\n var serviceProvider = services.BuildServiceProvider();\n var resolver = serviceProvider.GetRequiredService<IModelResolver>();\n\n // Act\n var resolution = await resolver.ResolveProviderAsync(\"unknown-model\");\n\n // Assert\n Assert.Null(resolution);\n }\n\n [Fact]\n public async Task UnifiedAgent_ShouldUpdateModelIdWhenForwardingToProviderAgent()\n {\n // Arrange\n var config = new AppConfig\n {\n Models = new[]\n {\n new ModelConfig\n {\n Id = \"gpt-4.1\",\n Capabilities = new ModelCapabilities\n {\n TokenLimits = new TokenLimits\n {\n MaxContextTokens = 4000,\n MaxOutputTokens = 1000,\n RecommendedMaxPromptTokens = 3000,\n },\n },\n Providers = new[]\n {\n new ProviderConfig\n {\n Name = \"OpenRouter\",\n ModelName = \"openai/gpt-4.1\", // Different from the model ID\n Priority = 1,\n Pricing = new PricingConfig { PromptPerMillion = 2.0, CompletionPerMillion = 8.0 },\n Tags = new[] { \"openai-compatible\" },\n },\n },\n },\n },\n ProviderRegistry = new Dictionary<string, ProviderConnectionInfo>\n {\n [\"OpenRouter\"] = new ProviderConnectionInfo\n {\n EndpointUrl = \"https://openrouter.ai/api/v1\",\n ApiKeyEnvironmentVariable = \"OPENROUTER_API_KEY\",\n Compatibility = \"OpenAI\",\n },\n },\n };\n\n var services = CreateServices(config);\n\n // Create a mock model resolver that returns a predefined resolution\n var mockModelResolver = new Mock<IModelResolver>();\n var expectedResolution = new ProviderResolution\n {\n Model = config.Models[0],\n Provider = config.Models[0].Providers[0],\n Connection = config.ProviderRegistry[\"OpenRouter\"],\n };\n\n mockModelResolver\n .Setup(r =>\n r.ResolveProviderAsync(\n It.IsAny<string>(),\n It.IsAny<ProviderSelectionCriteria>(),\n It.IsAny<CancellationToken>()\n )\n )\n .ReturnsAsync(expectedResolution);\n\n // Create a mock agent factory that captures the options passed to it\n var mockAgentFactory = new Mock<IProviderAgentFactory>();\n var mockAgent = new Mock<IStreamingAgent>();\n\n GenerateReplyOptions? capturedOptions = null;\n\n mockAgent\n .Setup(a =>\n a.GenerateReplyAsync(\n It.IsAny<IEnumerable<IMessage>>(),\n It.IsAny<GenerateReplyOptions>(),\n It.IsAny<CancellationToken>()\n )\n )\n .Callback<IEnumerable<IMessage>, GenerateReplyOptions?, CancellationToken>(\n (messages, options, ct) => capturedOptions = options\n )\n .ReturnsAsync(\n new List<IMessage>\n {\n new TextMessage { Role = Role.Assistant, Text = \"Test response\" },\n }\n );\n\n mockAgentFactory.Setup(f => f.CreateAgent(It.IsAny<ProviderResolution>())).Returns(mockAgent.Object);\n\n services.AddSingleton(mockModelResolver.Object);\n services.AddSingleton(mockAgentFactory.Object);\n services.AddSingleton<UnifiedAgent>();\n\n var serviceProvider = services.BuildServiceProvider();\n var unifiedAgent = serviceProvider.GetRequiredService<UnifiedAgent>();\n\n var messages = new List<IMessage>\n {\n new TextMessage { Role = Role.User, Text = \"Hello, world!\" },\n };\n\n var originalOptions = new GenerateReplyOptions\n {\n ModelId = \"gpt-4.1\", // User specifies the logical model ID\n Temperature = 0.7f,\n };\n\n // Act\n await unifiedAgent.GenerateReplyAsync(messages, originalOptions);\n\n // Assert\n Assert.NotNull(capturedOptions);\n Assert.Equal(\"openai/gpt-4.1\", capturedOptions.ModelId); // Should be updated to provider's model name\n Assert.Equal(0.7f, capturedOptions.Temperature); // Other properties should be preserved\n\n // Verify the mock was called\n mockAgent.Verify(\n a =>\n a.GenerateReplyAsync(\n It.IsAny<IEnumerable<IMessage>>(),\n It.Is<GenerateReplyOptions>(o => o.ModelId == \"openai/gpt-4.1\"),\n It.IsAny<CancellationToken>()\n ),\n Times.Once\n );\n }\n\n [Fact]\n public async Task UnifiedAgent_ShouldUpdateModelIdForStreamingRequests()\n {\n // Arrange\n var config = new AppConfig\n {\n Models = new[]\n {\n new ModelConfig\n {\n Id = \"claude-3-sonnet\",\n Capabilities = new ModelCapabilities\n {\n TokenLimits = new TokenLimits\n {\n MaxContextTokens = 4000,\n MaxOutputTokens = 1000,\n RecommendedMaxPromptTokens = 3000,\n },\n },\n Providers = new[]\n {\n new ProviderConfig\n {\n Name = \"OpenRouter\",\n ModelName = \"anthropic/claude-3-sonnet\", // Different from the model ID\n Priority = 1,\n Pricing = new PricingConfig { PromptPerMillion = 3.0, CompletionPerMillion = 15.0 },\n Tags = new[] { \"openai-compatible\" },\n },\n },\n },\n },\n ProviderRegistry = new Dictionary<string, ProviderConnectionInfo>\n {\n [\"OpenRouter\"] = new ProviderConnectionInfo\n {\n EndpointUrl = \"https://openrouter.ai/api/v1\",\n ApiKeyEnvironmentVariable = \"OPENROUTER_API_KEY\",\n Compatibility = \"OpenAI\",\n },\n },\n };\n\n var services = CreateServices(config);\n\n // Create a mock model resolver that returns a predefined resolution\n var mockModelResolver = new Mock<IModelResolver>();\n var expectedResolution = new ProviderResolution\n {\n Model = config.Models[0],\n Provider = config.Models[0].Providers[0],\n Connection = config.ProviderRegistry[\"OpenRouter\"],\n };\n\n mockModelResolver\n .Setup(r =>\n r.ResolveProviderAsync(\n It.IsAny<string>(),\n It.IsAny<ProviderSelectionCriteria>(),\n It.IsAny<CancellationToken>()\n )\n )\n .ReturnsAsync(expectedResolution);\n\n // Create a mock agent factory that captures the options passed to it\n var mockAgentFactory = new Mock<IProviderAgentFactory>();\n var mockAgent = new Mock<IStreamingAgent>();\n\n GenerateReplyOptions? capturedOptions = null;\n\n mockAgent\n .Setup(a =>\n a.GenerateReplyStreamingAsync(\n It.IsAny<IEnumerable<IMessage>>(),\n It.IsAny<GenerateReplyOptions>(),\n It.IsAny<CancellationToken>()\n )\n )\n .Callback<IEnumerable<IMessage>, GenerateReplyOptions?, CancellationToken>(\n (messages, options, ct) => capturedOptions = options\n )\n .ReturnsAsync(CreateEmptyAsyncEnumerable());\n\n mockAgentFactory.Setup(f => f.CreateStreamingAgent(It.IsAny<ProviderResolution>())).Returns(mockAgent.Object);\n\n services.AddSingleton(mockModelResolver.Object);\n services.AddSingleton(mockAgentFactory.Object);\n services.AddSingleton<UnifiedAgent>();\n\n var serviceProvider = services.BuildServiceProvider();\n var unifiedAgent = serviceProvider.GetRequiredService<UnifiedAgent>();\n\n var messages = new List<IMessage>\n {\n new TextMessage { Role = Role.User, Text = \"Hello, world!\" },\n };\n\n var originalOptions = new GenerateReplyOptions\n {\n ModelId = \"claude-3-sonnet\", // User specifies the logical model ID\n Temperature = 0.5f,\n MaxToken = 2000,\n };\n\n // Act\n await unifiedAgent.GenerateReplyStreamingAsync(messages, originalOptions);\n\n // Assert\n Assert.NotNull(capturedOptions);\n Assert.Equal(\"anthropic/claude-3-sonnet\", capturedOptions.ModelId); // Should be updated to provider's model name\n Assert.Equal(0.5f, capturedOptions.Temperature); // Other properties should be preserved\n Assert.Equal(2000, capturedOptions.MaxToken); // Other properties should be preserved\n\n // Verify the mock was called\n mockAgent.Verify(\n a =>\n a.GenerateReplyStreamingAsync(\n It.IsAny<IEnumerable<IMessage>>(),\n It.Is<GenerateReplyOptions>(o => o.ModelId == \"anthropic/claude-3-sonnet\"),\n It.IsAny<CancellationToken>()\n ),\n Times.Once\n );\n }\n\n [Fact]\n public async Task UnifiedAgent_ShouldCreateOptionsWithEffectiveModelNameWhenNullOptions()\n {\n // Arrange\n var config = new AppConfig\n {\n Models = new[]\n {\n new ModelConfig\n {\n Id = \"deepseek-r1\",\n Capabilities = new ModelCapabilities\n {\n TokenLimits = new TokenLimits\n {\n MaxContextTokens = 4000,\n MaxOutputTokens = 1000,\n RecommendedMaxPromptTokens = 3000,\n },\n },\n Providers = new[]\n {\n new ProviderConfig\n {\n Name = \"DeepSeek\",\n ModelName = \"deepseek-reasoner\", // Different from the model ID\n Priority = 1,\n Pricing = new PricingConfig { PromptPerMillion = 0.55, CompletionPerMillion = 2.19 },\n Tags = new[] { \"reasoning\" },\n },\n },\n },\n },\n ProviderRegistry = new Dictionary<string, ProviderConnectionInfo>\n {\n [\"DeepSeek\"] = new ProviderConnectionInfo\n {\n EndpointUrl = \"https://api.deepseek.com/v1\",\n ApiKeyEnvironmentVariable = \"DEEPSEEK_API_KEY\",\n Compatibility = \"OpenAI\",\n },\n },\n };\n\n var services = CreateServices(config);\n\n // Create a mock model resolver that returns a predefined resolution\n var mockModelResolver = new Mock<IModelResolver>();\n var expectedResolution = new ProviderResolution\n {\n Model = config.Models[0],\n Provider = config.Models[0].Providers[0],\n Connection = config.ProviderRegistry[\"DeepSeek\"],\n };\n\n mockModelResolver\n .Setup(r =>\n r.ResolveProviderAsync(\n It.IsAny<string>(),\n It.IsAny<ProviderSelectionCriteria>(),\n It.IsAny<CancellationToken>()\n )\n )\n .ReturnsAsync(expectedResolution);\n\n // Create a mock agent factory that captures the options passed to it\n var mockAgentFactory = new Mock<IProviderAgentFactory>();\n var mockAgent = new Mock<IStreamingAgent>();\n\n GenerateReplyOptions? capturedOptions = null;\n\n mockAgent\n .Setup(a =>\n a.GenerateReplyAsync(\n It.IsAny<IEnumerable<IMessage>>(),\n It.IsAny<GenerateReplyOptions>(),\n It.IsAny<CancellationToken>()\n )\n )\n .Callback<IEnumerable<IMessage>, GenerateReplyOptions?, CancellationToken>(\n (messages, options, ct) => capturedOptions = options\n )\n .ReturnsAsync(\n new List<IMessage>\n {\n new TextMessage { Role = Role.Assistant, Text = \"Test response\" },\n }\n );\n\n mockAgentFactory.Setup(f => f.CreateAgent(It.IsAny<ProviderResolution>())).Returns(mockAgent.Object);\n\n services.AddSingleton(mockModelResolver.Object);\n services.AddSingleton(mockAgentFactory.Object);\n services.AddSingleton<UnifiedAgent>();\n\n var serviceProvider = services.BuildServiceProvider();\n var unifiedAgent = serviceProvider.GetRequiredService<UnifiedAgent>();\n\n var messages = new List<IMessage>\n {\n new TextMessage { Role = Role.User, Text = \"Hello, world!\" },\n };\n\n // Pass null options but the agent should still work by getting model from context\n // We need to test this differently since we can't pass null options and expect it to work\n // Let's test with minimal options instead\n var minimalOptions = new GenerateReplyOptions { ModelId = \"deepseek-r1\" };\n\n // Act\n await unifiedAgent.GenerateReplyAsync(messages, minimalOptions);\n\n // Assert\n Assert.NotNull(capturedOptions);\n Assert.Equal(\"deepseek-reasoner\", capturedOptions.ModelId); // Should be updated to provider's model name\n\n // Verify the mock was called\n mockAgent.Verify(\n a =>\n a.GenerateReplyAsync(\n It.IsAny<IEnumerable<IMessage>>(),\n It.Is<GenerateReplyOptions>(o => o.ModelId == \"deepseek-reasoner\"),\n It.IsAny<CancellationToken>()\n ),\n Times.Once\n );\n }\n}\n"}, "22856bf8c6dd4e3bfcf39c580eb1dfed8a290f73": {"t": 1785756734.6593006, "n": 20701, "h": "35ac9c13e8a3a6cb", "c": "using System.Linq;\nusing AchieveAi.LmDotnetTools.LmConfig.Agents;\nusing AchieveAi.LmDotnetTools.LmConfig.Capabilities;\nusing AchieveAi.LmDotnetTools.LmConfig.Models;\nusing AchieveAi.LmDotnetTools.LmCore.Agents;\nusing AchieveAi.LmDotnetTools.LmCore.Messages;\nusing Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.Logging;\nusing Microsoft.Extensions.Options;\nusing Moq;\nusing Xunit;\n\nnamespace LmConfig.Tests;\n\npublic class UnifiedAgentTests\n{\n private static ServiceCollection CreateServices(AppConfig config)\n {\n var services = new ServiceCollection();\n services.AddLogging();\n services.AddSingleton(Options.Create(config));\n return services;\n }\n\n private static async IAsyncEnumerable<IMessage> CreateEmptyAsyncEnumerable()\n {\n await Task.CompletedTask; // Make it async\n yield break; // Return empty sequence\n }\n\n [Fact]\n public void UnifiedAgent_ShouldImplementIAgent()\n {\n // Arrange\n var config = new AppConfig\n {\n Models = new[]\n {\n new ModelConfig\n {\n Id = \"test-model\",\n Capabilities = new ModelCapabilities\n {\n TokenLimits = new TokenLimits\n {\n MaxContextTokens = 4000,\n MaxOutputTokens = 1000,\n RecommendedMaxPromptTokens = 3000,\n },\n },\n Providers = new[]\n {\n new ProviderConfig\n {\n Name = \"TestProvider\",\n ModelName = \"test-model-name\",\n Priority = 1,\n Pricing = new PricingConfig { PromptPerMillion = 1.0, CompletionPerMillion = 2.0 },\n Tags = new[] { \"test\" },\n },\n },\n },\n },\n ProviderRegistry = new Dictionary<string, ProviderConnectionInfo>\n {\n [\"TestProvider\"] = new ProviderConnectionInfo\n {\n EndpointUrl = \"https://test.example.com\",\n ApiKeyEnvironmentVariable = \"TEST_API_KEY\",\n Compatibility = \"OpenAI\",\n },\n },\n };\n\n var services = CreateServices(config);\n services.AddSingleton<IModelResolver, ModelResolver>();\n services.AddSingleton<IProviderAgentFactory, ProviderAgentFactory>();\n services.AddSingleton<UnifiedAgent>();\n\n var serviceProvider = services.BuildServiceProvider();\n\n // Act\n var agent = serviceProvider.GetRequiredService<UnifiedAgent>();\n\n // Assert\n Assert.NotNull(agent);\n Assert.IsAssignableFrom<IAgent>(agent);\n Assert.IsAssignableFrom<IStreamingAgent>(agent);\n }\n\n [Fact]\n public async Task ModelResolver_ShouldResolveProvider()\n {\n // Arrange\n var config = new AppConfig\n {\n Models = new[]\n {\n new ModelConfig\n {\n Id = \"test-model\",\n Capabilities = new ModelCapabilities\n {\n TokenLimits = new TokenLimits\n {\n MaxContextTokens = 4000,\n MaxOutputTokens = 1000,\n RecommendedMaxPromptTokens = 3000,\n },\n },\n Providers = new[]\n {\n new ProviderConfig\n {\n Name = \"TestProvider\",\n ModelName = \"test-model-name\",\n Priority = 1,\n Pricing = new PricingConfig { PromptPerMillion = 1.0, CompletionPerMillion = 2.0 },\n Tags = new[] { \"test\" },\n },\n },\n },\n },\n ProviderRegistry = new Dictionary<string, ProviderConnectionInfo>\n {\n [\"TestProvider\"] = new ProviderConnectionInfo\n {\n EndpointUrl = \"https://test.example.com\",\n ApiKeyEnvironmentVariable = \"TEST_API_KEY\",\n Compatibility = \"OpenAI\",\n },\n },\n };\n\n // Set up the required environment variable for the test\n Environment.SetEnvironmentVariable(\"TEST_API_KEY\", \"test-api-key\");\n\n try\n {\n var services = CreateServices(config);\n services.AddSingleton<IModelResolver, ModelResolver>();\n\n var serviceProvider = services.BuildServiceProvider();\n var resolver = serviceProvider.GetRequiredService<IModelResolver>();\n\n // Act\n var resolution = await resolver.ResolveProviderAsync(\"test-model\");\n\n // Assert\n Assert.NotNull(resolution);\n Assert.Equal(\"test-model\", resolution.Model.Id);\n Assert.Equal(\"TestProvider\", resolution.Provider.Name);\n Assert.Equal(\"test-model-name\", resolution.Provider.ModelName);\n Assert.NotNull(resolution.Connection);\n Assert.Equal(\"https://test.example.com\", resolution.Connection.EndpointUrl);\n }\n finally\n {\n // Clean up the environment variable\n Environment.SetEnvironmentVariable(\"TEST_API_KEY\", null);\n }\n }\n\n [Fact]\n public async Task ModelResolver_ShouldReturnNullForUnknownModel()\n {\n // Arrange\n var config = new AppConfig\n {\n Models = Array.Empty<ModelConfig>(),\n ProviderRegistry = new Dictionary<string, ProviderConnectionInfo>(),\n };\n\n var services = CreateServices(config);\n services.AddSingleton<IModelResolver, ModelResolver>();\n\n var serviceProvider = services.BuildServiceProvider();\n var resolver = serviceProvider.GetRequiredService<IModelResolver>();\n\n // Act\n var resolution = await resolver.ResolveProviderAsync(\"unknown-model\");\n\n // Assert\n Assert.Null(resolution);\n }\n\n [Fact]\n public async Task UnifiedAgent_ShouldUpdateModelIdWhenForwardingToProviderAgent()\n {\n // Arrange\n var config = new AppConfig\n {\n Models = new[]\n {\n new ModelConfig\n {\n Id = \"gpt-4.1\",\n Capabilities = new ModelCapabilities\n {\n TokenLimits = new TokenLimits\n {\n MaxContextTokens = 4000,\n MaxOutputTokens = 1000,\n RecommendedMaxPromptTokens = 3000,\n },\n },\n Providers = new[]\n {\n new ProviderConfig\n {\n Name = \"OpenRouter\",\n ModelName = \"openai/gpt-4.1\", // Different from the model ID\n Priority = 1,\n Pricing = new PricingConfig { PromptPerMillion = 2.0, CompletionPerMillion = 8.0 },\n Tags = new[] { \"openai-compatible\" },\n },\n },\n },\n },\n ProviderRegistry = new Dictionary<string, ProviderConnectionInfo>\n {\n [\"OpenRouter\"] = new ProviderConnectionInfo\n {\n EndpointUrl = \"https://openrouter.ai/api/v1\",\n ApiKeyEnvironmentVariable = \"OPENROUTER_API_KEY\",\n Compatibility = \"OpenAI\",\n },\n },\n };\n\n var services = CreateServices(config);\n\n // Create a mock model resolver that returns a predefined resolution\n var mockModelResolver = new Mock<IModelResolver>();\n var expectedResolution = new ProviderResolution\n {\n Model = config.Models[0],\n Provider = config.Models[0].Providers[0],\n Connection = config.ProviderRegistry[\"OpenRouter\"],\n };\n\n mockModelResolver\n .Setup(r =>\n r.ResolveProviderAsync(\n It.IsAny<string>(),\n It.IsAny<ProviderSelectionCriteria>(),\n It.IsAny<CancellationToken>()\n )\n )\n .ReturnsAsync(expectedResolution);\n\n // Create a mock agent factory that captures the options passed to it\n var mockAgentFactory = new Mock<IProviderAgentFactory>();\n var mockAgent = new Mock<IStreamingAgent>();\n\n GenerateReplyOptions? capturedOptions = null;\n\n mockAgent\n .Setup(a =>\n a.GenerateReplyAsync(\n It.IsAny<IEnumerable<IMessage>>(),\n It.IsAny<GenerateReplyOptions>(),\n It.IsAny<CancellationToken>()\n )\n )\n .Callback<IEnumerable<IMessage>, GenerateReplyOptions?, CancellationToken>(\n (messages, options, ct) => capturedOptions = options\n )\n .ReturnsAsync(\n new List<IMessage>\n {\n new TextMessage { Role = Role.Assistant, Text = \"Test response\" },\n }\n );\n\n mockAgentFactory.Setup(f => f.CreateAgent(It.IsAny<ProviderResolution>())).Returns(mockAgent.Object);\n\n services.AddSingleton(mockModelResolver.Object);\n services.AddSingleton(mockAgentFactory.Object);\n services.AddSingleton<UnifiedAgent>();\n\n var serviceProvider = services.BuildServiceProvider();\n var unifiedAgent = serviceProvider.GetRequiredService<UnifiedAgent>();\n\n var messages = new List<IMessage>\n {\n new TextMessage { Role = Role.User, Text = \"Hello, world!\" },\n };\n\n var originalOptions = new GenerateReplyOptions\n {\n ModelId = \"gpt-4.1\", // User specifies the logical model ID\n Temperature = 0.7f,\n };\n\n // Act\n await unifiedAgent.GenerateReplyAsync(messages, originalOptions);\n\n // Assert\n Assert.NotNull(capturedOptions);\n Assert.Equal(\"openai/gpt-4.1\", capturedOptions.ModelId); // Should be updated to provider's model name\n Assert.Equal(0.7f, capturedOptions.Temperature); // Other properties should be preserved\n\n // Verify the mock was called\n mockAgent.Verify(\n a =>\n a.GenerateReplyAsync(\n It.IsAny<IEnumerable<IMessage>>(),\n It.Is<GenerateReplyOptions>(o => o.ModelId == \"openai/gpt-4.1\"),\n It.IsAny<CancellationToken>()\n ),\n Times.Once\n );\n }\n\n [Fact]\n public async Task UnifiedAgent_ShouldUpdateModelIdForStreamingRequests()\n {\n // Arrange\n var config = new AppConfig\n {\n Models = new[]\n {\n new ModelConfig\n {\n Id = \"claude-3-sonnet\",\n Capabilities = new ModelCapabilities\n {\n TokenLimits = new TokenLimits\n {\n MaxContextTokens = 4000,\n MaxOutputTokens = 1000,\n RecommendedMaxPromptTokens = 3000,\n },\n },\n Providers = new[]\n {\n new ProviderConfig\n {\n Name = \"OpenRouter\",\n ModelName = \"anthropic/claude-3-sonnet\", // Different from the model ID\n Priority = 1,\n Pricing = new PricingConfig { PromptPerMillion = 3.0, CompletionPerMillion = 15.0 },\n Tags = new[] { \"openai-compatible\" },\n },\n },\n },\n },\n ProviderRegistry = new Dictionary<string, ProviderConnectionInfo>\n {\n [\"OpenRouter\"] = new ProviderConnectionInfo\n {\n EndpointUrl = \"https://openrouter.ai/api/v1\",\n ApiKeyEnvironmentVariable = \"OPENROUTER_API_KEY\",\n Compatibility = \"OpenAI\",\n },\n },\n };\n\n var services = CreateServices(config);\n\n // Create a mock model resolver that returns a predefined resolution\n var mockModelResolver = new Mock<IModelResolver>();\n var expectedResolution = new ProviderResolution\n {\n Model = config.Models[0],\n Provider = config.Models[0].Providers[0],\n Connection = config.ProviderRegistry[\"OpenRouter\"],\n };\n\n mockModelResolver\n .Setup(r =>\n r.ResolveProviderAsync(\n It.IsAny<string>(),\n It.IsAny<ProviderSelectionCriteria>(),\n It.IsAny<CancellationToken>()\n )\n )\n .ReturnsAsync(expectedResolution);\n\n // Create a mock agent factory that captures the options passed to it\n var mockAgentFactory = new Mock<IProviderAgentFactory>();\n var mockAgent = new Mock<IStreamingAgent>();\n\n GenerateReplyOptions? capturedOptions = null;\n\n mockAgent\n .Setup(a =>\n a.GenerateReplyStreamingAsync(\n It.IsAny<IEnumerable<IMessage>>(),\n It.IsAny<GenerateReplyOptions>(),\n It.IsAny<CancellationToken>()\n )\n )\n .Callback<IEnumerable<IMessage>, GenerateReplyOptions?, CancellationToken>(\n (messages, options, ct) => capturedOptions = options\n )\n .ReturnsAsync(CreateEmptyAsyncEnumerable());\n\n mockAgentFactory.Setup(f => f.CreateStreamingAgent(It.IsAny<ProviderResolution>())).Returns(mockAgent.Object);\n\n services.AddSingleton(mockModelResolver.Object);\n services.AddSingleton(mockAgentFactory.Object);\n services.AddSingleton<UnifiedAgent>();\n\n var serviceProvider = services.BuildServiceProvider();\n var unifiedAgent = serviceProvider.GetRequiredService<UnifiedAgent>();\n\n var messages = new List<IMessage>\n {\n new TextMessage { Role = Role.User, Text = \"Hello, world!\" },\n };\n\n var originalOptions = new GenerateReplyOptions\n {\n ModelId = \"claude-3-sonnet\", // User specifies the logical model ID\n Temperature = 0.5f,\n MaxToken = 2000,\n };\n\n // Act\n await unifiedAgent.GenerateReplyStreamingAsync(messages, originalOptions);\n\n // Assert\n Assert.NotNull(capturedOptions);\n Assert.Equal(\"anthropic/claude-3-sonnet\", capturedOptions.ModelId); // Should be updated to provider's model name\n Assert.Equal(0.5f, capturedOptions.Temperature); // Other properties should be preserved\n Assert.Equal(2000, capturedOptions.MaxToken); // Other properties should be preserved\n\n // Verify the mock was called\n mockAgent.Verify(\n a =>\n a.GenerateReplyStreamingAsync(\n It.IsAny<IEnumerable<IMessage>>(),\n It.Is<GenerateReplyOptions>(o => o.ModelId == \"anthropic/claude-3-sonnet\"),\n It.IsAny<CancellationToken>()\n ),\n Times.Once\n );\n }\n\n [Fact]\n public async Task UnifiedAgent_ShouldCreateOptionsWithEffectiveModelNameWhenNullOptions()\n {\n // Arrange\n var config = new AppConfig\n {\n Models = new[]\n {\n new ModelConfig\n {\n Id = \"deepseek-r1\",\n Capabilities = new ModelCapabilities\n {\n TokenLimits = new TokenLimits\n {\n MaxContextTokens = 4000,\n MaxOutputTokens = 1000,\n RecommendedMaxPromptTokens = 3000,\n },\n },\n Providers = new[]\n {\n new ProviderConfig\n {\n Name = \"DeepSeek\",\n ModelName = \"deepseek-reasoner\", // Different from the model ID\n Priority = 1,\n Pricing = new PricingConfig { PromptPerMillion = 0.55, CompletionPerMillion = 2.19 },\n Tags = new[] { \"reasoning\" },\n },\n },\n },\n },\n ProviderRegistry = new Dictionary<string, ProviderConnectionInfo>\n {\n [\"DeepSeek\"] = new ProviderConnectionInfo\n {\n EndpointUrl = \"https://api.deepseek.com/v1\",\n ApiKeyEnvironmentVariable = \"DEEPSEEK_API_KEY\",\n Compatibility = \"OpenAI\",\n },\n },\n };\n\n var services = CreateServices(config);\n\n // Create a mock model resolver that returns a predefined resolution\n var mockModelResolver = new Mock<IModelResolver>();\n var expectedResolution = new ProviderResolution\n {\n Model = config.Models[0],\n Provider = config.Models[0].Providers[0],\n Connection = config.ProviderRegistry[\"DeepSeek\"],\n };\n\n mockModelResolver\n .Setup(r =>\n r.ResolveProviderAsync(\n It.IsAny<string>(),\n It.IsAny<ProviderSelectionCriteria>(),\n It.IsAny<CancellationToken>()\n )\n )\n .ReturnsAsync(expectedResolution);\n\n // Create a mock agent factory that captures the options passed to it\n var mockAgentFactory = new Mock<IProviderAgentFactory>();\n var mockAgent = new Mock<IStreamingAgent>();\n\n GenerateReplyOptions? capturedOptions = null;\n\n mockAgent\n .Setup(a =>\n a.GenerateReplyAsync(\n It.IsAny<IEnumerable<IMessage>>(),\n It.IsAny<GenerateReplyOptions>(),\n It.IsAny<CancellationToken>()\n )\n )\n .Callback<IEnumerable<IMessage>, GenerateReplyOptions?, CancellationToken>(\n (messages, options, ct) => capturedOptions = options\n )\n .ReturnsAsync(\n new List<IMessage>\n {\n new TextMessage { Role = Role.Assistant, Text = \"Test response\" },\n }\n );\n\n mockAgentFactory.Setup(f => f.CreateAgent(It.IsAny<ProviderResolution>())).Returns(mockAgent.Object);\n\n services.AddSingleton(mockModelResolver.Object);\n services.AddSingleton(mockAgentFactory.Object);\n services.AddSingleton<UnifiedAgent>();\n\n var serviceProvider = services.BuildServiceProvider();\n var unifiedAgent = serviceProvider.GetRequiredService<UnifiedAgent>();\n\n var messages = new List<IMessage>\n {\n new TextMessage { Role = Role.User, Text = \"Hello, world!\" },\n };\n\n // Pass null options but the agent should still work by getting model from context\n // We need to test this differently since we can't pass null options and expect it to work\n // Let's test with minimal options instead\n var minimalOptions = new GenerateReplyOptions { ModelId = \"deepseek-r1\" };\n\n // Act\n await unifiedAgent.GenerateReplyAsync(messages, minimalOptions);\n\n // Assert\n Assert.NotNull(capturedOptions);\n Assert.Equal(\"deepseek-reasoner\", capturedOptions.ModelId); // Should be updated to provider's model name\n\n // Verify the mock was called\n mockAgent.Verify(\n a =>\n a.GenerateReplyAsync(\n It.IsAny<IEnumerable<IMessage>>(),\n It.Is<GenerateReplyOptions>(o => o.ModelId == \"deepseek-reasoner\"),\n It.IsAny<CancellationToken>()\n ),\n Times.Once\n );\n }\n}\n"}, "d482ab4fa5c811072dad32d7029d2645b00a2f7c": {"t": 1785756736.0595503, "n": 21429, "h": "82d0af8169181e3a", "c": "using System.Linq;\nusing AchieveAi.LmDotnetTools.LmConfig.Agents;\nusing AchieveAi.LmDotnetTools.LmConfig.Capabilities;\nusing AchieveAi.LmDotnetTools.LmConfig.Models;\nusing AchieveAi.LmDotnetTools.LmCore.Agents;\nusing AchieveAi.LmDotnetTools.LmCore.Messages;\nusing Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.Logging;\nusing Microsoft.Extensions.Options;\nusing Moq;\nusing Xunit;\n\nnamespace LmConfig.Tests;\n\npublic class UnifiedAgentTests\n{\n private static ServiceCollection CreateServices(AppConfig config)\n {\n var services = new ServiceCollection();\n services.AddLogging();\n services.AddSingleton(Options.Create(config));\n return services;\n }\n\n private static async IAsyncEnumerable<IMessage> CreateEmptyAsyncEnumerable()\n {\n await Task.CompletedTask; // Make it async\n yield break; // Return empty sequence\n }\n\n [Fact]\n public void UnifiedAgent_ShouldImplementIAgent()\n {\n // Arrange\n var config = new AppConfig\n {\n Models = new[]\n {\n new ModelConfig\n {\n Id = \"test-model\",\n Capabilities = new ModelCapabilities\n {\n TokenLimits = new TokenLimits\n {\n MaxContextTokens = 4000,\n MaxOutputTokens = 1000,\n RecommendedMaxPromptTokens = 3000,\n },\n },\n Providers = new[]\n {\n new ProviderConfig\n {\n Name = \"TestProvider\",\n ModelName = \"test-model-name\",\n Priority = 1,\n Pricing = new PricingConfig\n {\n PromptPerMillion = 1.0,\n CompletionPerMillion = 2.0,\n },\n Tags = new[] { \"test\" },\n },\n },\n },\n },\n ProviderRegistry = new Dictionary<string, ProviderConnectionInfo>\n {\n [\"TestProvider\"] = new ProviderConnectionInfo\n {\n EndpointUrl = \"https://test.example.com\",\n ApiKeyEnvironmentVariable = \"TEST_API_KEY\",\n Compatibility = \"OpenAI\",\n },\n },\n };\n\n var services = CreateServices(config);\n services.AddSingleton<IModelResolver, ModelResolver>();\n services.AddSingleton<IProviderAgentFactory, ProviderAgentFactory>();\n services.AddSingleton<UnifiedAgent>();\n\n var serviceProvider = services.BuildServiceProvider();\n\n // Act\n var agent = serviceProvider.GetRequiredService<UnifiedAgent>();\n\n // Assert\n Assert.NotNull(agent);\n Assert.IsAssignableFrom<IAgent>(agent);\n Assert.IsAssignableFrom<IStreamingAgent>(agent);\n }\n\n [Fact]\n public async Task ModelResolver_ShouldResolveProvider()\n {\n // Arrange\n var config = new AppConfig\n {\n Models = new[]\n {\n new ModelConfig\n {\n Id = \"test-model\",\n Capabilities = new ModelCapabilities\n {\n TokenLimits = new TokenLimits\n {\n MaxContextTokens = 4000,\n MaxOutputTokens = 1000,\n RecommendedMaxPromptTokens = 3000,\n },\n },\n Providers = new[]\n {\n new ProviderConfig\n {\n Name = \"TestProvider\",\n ModelName = \"test-model-name\",\n Priority = 1,\n Pricing = new PricingConfig\n {\n PromptPerMillion = 1.0,\n CompletionPerMillion = 2.0,\n },\n Tags = new[] { \"test\" },\n },\n },\n },\n },\n ProviderRegistry = new Dictionary<string, ProviderConnectionInfo>\n {\n [\"TestProvider\"] = new ProviderConnectionInfo\n {\n EndpointUrl = \"https://test.example.com\",\n ApiKeyEnvironmentVariable = \"TEST_API_KEY\",\n Compatibility = \"OpenAI\",\n },\n },\n };\n\n // Set up the required environment variable for the test\n Environment.SetEnvironmentVariable(\"TEST_API_KEY\", \"test-api-key\");\n\n try\n {\n var services = CreateServices(config);\n services.AddSingleton<IModelResolver, ModelResolver>();\n\n var serviceProvider = services.BuildServiceProvider();\n var resolver = serviceProvider.GetRequiredService<IModelResolver>();\n\n // Act\n var resolution = await resolver.ResolveProviderAsync(\"test-model\");\n\n // Assert\n Assert.NotNull(resolution);\n Assert.Equal(\"test-model\", resolution.Model.Id);\n Assert.Equal(\"TestProvider\", resolution.Provider.Name);\n Assert.Equal(\"test-model-name\", resolution.Provider.ModelName);\n Assert.NotNull(resolution.Connection);\n Assert.Equal(\"https://test.example.com\", resolution.Connection.EndpointUrl);\n }\n finally\n {\n // Clean up the environment variable\n Environment.SetEnvironmentVariable(\"TEST_API_KEY\", null);\n }\n }\n\n [Fact]\n public async Task ModelResolver_ShouldReturnNullForUnknownModel()\n {\n // Arrange\n var config = new AppConfig\n {\n Models = Array.Empty<ModelConfig>(),\n ProviderRegistry = new Dictionary<string, ProviderConnectionInfo>(),\n };\n\n var services = CreateServices(config);\n services.AddSingleton<IModelResolver, ModelResolver>();\n\n var serviceProvider = services.BuildServiceProvider();\n var resolver = serviceProvider.GetRequiredService<IModelResolver>();\n\n // Act\n var resolution = await resolver.ResolveProviderAsync(\"unknown-model\");\n\n // Assert\n Assert.Null(resolution);\n }\n\n [Fact]\n public async Task UnifiedAgent_ShouldUpdateModelIdWhenForwardingToProviderAgent()\n {\n // Arrange\n var config = new AppConfig\n {\n Models = new[]\n {\n new ModelConfig\n {\n Id = \"gpt-4.1\",\n Capabilities = new ModelCapabilities\n {\n TokenLimits = new TokenLimits\n {\n MaxContextTokens = 4000,\n MaxOutputTokens = 1000,\n RecommendedMaxPromptTokens = 3000,\n },\n },\n Providers = new[]\n {\n new ProviderConfig\n {\n Name = \"OpenRouter\",\n ModelName = \"openai/gpt-4.1\", // Different from the model ID\n Priority = 1,\n Pricing = new PricingConfig\n {\n PromptPerMillion = 2.0,\n CompletionPerMillion = 8.0,\n },\n Tags = new[] { \"openai-compatible\" },\n },\n },\n },\n },\n ProviderRegistry = new Dictionary<string, ProviderConnectionInfo>\n {\n [\"OpenRouter\"] = new ProviderConnectionInfo\n {\n EndpointUrl = \"https://openrouter.ai/api/v1\",\n ApiKeyEnvironmentVariable = \"OPENROUTER_API_KEY\",\n Compatibility = \"OpenAI\",\n },\n },\n };\n\n var services = CreateServices(config);\n\n // Create a mock model resolver that returns a predefined resolution\n var mockModelResolver = new Mock<IModelResolver>();\n var expectedResolution = new ProviderResolution\n {\n Model = config.Models.First(),\n Provider = config.Models.First().Providers.First(),\n Connection = config.ProviderRegistry[\"OpenRouter\"],\n };\n\n mockModelResolver\n .Setup(r =>\n r.ResolveProviderAsync(\n It.IsAny<string>(),\n It.IsAny<ProviderSelectionCriteria>(),\n It.IsAny<CancellationToken>()\n )\n )\n .ReturnsAsync(expectedResolution);\n\n // Create a mock agent factory that captures the options passed to it\n var mockAgentFactory = new Mock<IProviderAgentFactory>();\n var mockAgent = new Mock<IStreamingAgent>();\n\n GenerateReplyOptions? capturedOptions = null;\n\n mockAgent\n .Setup(a =>\n a.GenerateReplyAsync(\n It.IsAny<IEnumerable<IMessage>>(),\n It.IsAny<GenerateReplyOptions>(),\n It.IsAny<CancellationToken>()\n )\n )\n .Callback<IEnumerable<IMessage>, GenerateReplyOptions?, CancellationToken>(\n (messages, options, ct) => capturedOptions = options\n )\n .ReturnsAsync(\n new List<IMessage>\n {\n new TextMessage { Role = Role.Assistant, Text = \"Test response\" },\n }\n );\n\n mockAgentFactory\n .Setup(f => f.CreateAgent(It.IsAny<ProviderResolution>()))\n .Returns(mockAgent.Object);\n\n services.AddSingleton(mockModelResolver.Object);\n services.AddSingleton(mockAgentFactory.Object);\n services.AddSingleton<UnifiedAgent>();\n\n var serviceProvider = services.BuildServiceProvider();\n var unifiedAgent = serviceProvider.GetRequiredService<UnifiedAgent>();\n\n var messages = new List<IMessage>\n {\n new TextMessage { Role = Role.User, Text = \"Hello, world!\" },\n };\n\n var originalOptions = new GenerateReplyOptions\n {\n ModelId = \"gpt-4.1\", // User specifies the logical model ID\n Temperature = 0.7f,\n };\n\n // Act\n await unifiedAgent.GenerateReplyAsync(messages, originalOptions);\n\n // Assert\n Assert.NotNull(capturedOptions);\n Assert.Equal(\"openai/gpt-4.1\", capturedOptions.ModelId); // Should be updated to provider's model name\n Assert.Equal(0.7f, capturedOptions.Temperature); // Other properties should be preserved\n\n // Verify the mock was called\n mockAgent.Verify(\n a =>\n a.GenerateReplyAsync(\n It.IsAny<IEnumerable<IMessage>>(),\n It.Is<GenerateReplyOptions>(o => o.ModelId == \"openai/gpt-4.1\"),\n It.IsAny<CancellationToken>()\n ),\n Times.Once\n );\n }\n\n [Fact]\n public async Task UnifiedAgent_ShouldUpdateModelIdForStreamingRequests()\n {\n // Arrange\n var config = new AppConfig\n {\n Models = new[]\n {\n new ModelConfig\n {\n Id = \"claude-3-sonnet\",\n Capabilities = new ModelCapabilities\n {\n TokenLimits = new TokenLimits\n {\n MaxContextTokens = 4000,\n MaxOutputTokens = 1000,\n RecommendedMaxPromptTokens = 3000,\n },\n },\n Providers = new[]\n {\n new ProviderConfig\n {\n Name = \"OpenRouter\",\n ModelName = \"anthropic/claude-3-sonnet\", // Different from the model ID\n Priority = 1,\n Pricing = new PricingConfig\n {\n PromptPerMillion = 3.0,\n CompletionPerMillion = 15.0,\n },\n Tags = new[] { \"openai-compatible\" },\n },\n },\n },\n },\n ProviderRegistry = new Dictionary<string, ProviderConnectionInfo>\n {\n [\"OpenRouter\"] = new ProviderConnectionInfo\n {\n EndpointUrl = \"https://openrouter.ai/api/v1\",\n ApiKeyEnvironmentVariable = \"OPENROUTER_API_KEY\",\n Compatibility = \"OpenAI\",\n },\n },\n };\n\n var services = CreateServices(config);\n\n // Create a mock model resolver that returns a predefined resolution\n var mockModelResolver = new Mock<IModelResolver>();\n var expectedResolution = new ProviderResolution\n {\n Model = config.Models.First(),\n Provider = config.Models.First().Providers.First(),\n Connection = config.ProviderRegistry[\"OpenRouter\"],\n };\n\n mockModelResolver\n .Setup(r =>\n r.ResolveProviderAsync(\n It.IsAny<string>(),\n It.IsAny<ProviderSelectionCriteria>(),\n It.IsAny<CancellationToken>()\n )\n )\n .ReturnsAsync(expectedResolution);\n\n // Create a mock agent factory that captures the options passed to it\n var mockAgentFactory = new Mock<IProviderAgentFactory>();\n var mockAgent = new Mock<IStreamingAgent>();\n\n GenerateReplyOptions? capturedOptions = null;\n\n mockAgent\n .Setup(a =>\n a.GenerateReplyStreamingAsync(\n It.IsAny<IEnumerable<IMessage>>(),\n It.IsAny<GenerateReplyOptions>(),\n It.IsAny<CancellationToken>()\n )\n )\n .Callback<IEnumerable<IMessage>, GenerateReplyOptions?, CancellationToken>(\n (messages, options, ct) => capturedOptions = options\n )\n .ReturnsAsync(CreateEmptyAsyncEnumerable());\n\n mockAgentFactory\n .Setup(f => f.CreateStreamingAgent(It.IsAny<ProviderResolution>()))\n .Returns(mockAgent.Object);\n\n services.AddSingleton(mockModelResolver.Object);\n services.AddSingleton(mockAgentFactory.Object);\n services.AddSingleton<UnifiedAgent>();\n\n var serviceProvider = services.BuildServiceProvider();\n var unifiedAgent = serviceProvider.GetRequiredService<UnifiedAgent>();\n\n var messages = new List<IMessage>\n {\n new TextMessage { Role = Role.User, Text = \"Hello, world!\" },\n };\n\n var originalOptions = new GenerateReplyOptions\n {\n ModelId = \"claude-3-sonnet\", // User specifies the logical model ID\n Temperature = 0.5f,\n MaxToken = 2000,\n };\n\n // Act\n await unifiedAgent.GenerateReplyStreamingAsync(messages, originalOptions);\n\n // Assert\n Assert.NotNull(capturedOptions);\n Assert.Equal(\"anthropic/claude-3-sonnet\", capturedOptions.ModelId); // Should be updated to provider's model name\n Assert.Equal(0.5f, capturedOptions.Temperature); // Other properties should be preserved\n Assert.Equal(2000, capturedOptions.MaxToken); // Other properties should be preserved\n\n // Verify the mock was called\n mockAgent.Verify(\n a =>\n a.GenerateReplyStreamingAsync(\n It.IsAny<IEnumerable<IMessage>>(),\n It.Is<GenerateReplyOptions>(o => o.ModelId == \"anthropic/claude-3-sonnet\"),\n It.IsAny<CancellationToken>()\n ),\n Times.Once\n );\n }\n\n [Fact]\n public async Task UnifiedAgent_ShouldCreateOptionsWithEffectiveModelNameWhenNullOptions()\n {\n // Arrange\n var config = new AppConfig\n {\n Models = new[]\n {\n new ModelConfig\n {\n Id = \"deepseek-r1\",\n Capabilities = new ModelCapabilities\n {\n TokenLimits = new TokenLimits\n {\n MaxContextTokens = 4000,\n MaxOutputTokens = 1000,\n RecommendedMaxPromptTokens = 3000,\n },\n },\n Providers = new[]\n {\n new ProviderConfig\n {\n Name = \"DeepSeek\",\n ModelName = \"deepseek-reasoner\", // Different from the model ID\n Priority = 1,\n Pricing = new PricingConfig\n {\n PromptPerMillion = 0.55,\n CompletionPerMillion = 2.19,\n },\n Tags = new[] { \"reasoning\" },\n },\n },\n },\n },\n ProviderRegistry = new Dictionary<string, ProviderConnectionInfo>\n {\n [\"DeepSeek\"] = new ProviderConnectionInfo\n {\n EndpointUrl = \"https://api.deepseek.com/v1\",\n ApiKeyEnvironmentVariable = \"DEEPSEEK_API_KEY\",\n Compatibility = \"OpenAI\",\n },\n },\n };\n\n var services = CreateServices(config);\n\n // Create a mock model resolver that returns a predefined resolution\n var mockModelResolver = new Mock<IModelResolver>();\n var expectedResolution = new ProviderResolution\n {\n Model = config.Models.First(),\n Provider = config.Models.First().Providers.First(),\n Connection = config.ProviderRegistry[\"DeepSeek\"],\n };\n\n mockModelResolver\n .Setup(r =>\n r.ResolveProviderAsync(\n It.IsAny<string>(),\n It.IsAny<ProviderSelectionCriteria>(),\n It.IsAny<CancellationToken>()\n )\n )\n .ReturnsAsync(expectedResolution);\n\n // Create a mock agent factory that captures the options passed to it\n var mockAgentFactory = new Mock<IProviderAgentFactory>();\n var mockAgent = new Mock<IStreamingAgent>();\n\n GenerateReplyOptions? capturedOptions = null;\n\n mockAgent\n .Setup(a =>\n a.GenerateReplyAsync(\n It.IsAny<IEnumerable<IMessage>>(),\n It.IsAny<GenerateReplyOptions>(),\n It.IsAny<CancellationToken>()\n )\n )\n .Callback<IEnumerable<IMessage>, GenerateReplyOptions?, CancellationToken>(\n (messages, options, ct) => capturedOptions = options\n )\n .ReturnsAsync(\n new List<IMessage>\n {\n new TextMessage { Role = Role.Assistant, Text = \"Test response\" },\n }\n );\n\n mockAgentFactory\n .Setup(f => f.CreateAgent(It.IsAny<ProviderResolution>()))\n .Returns(mockAgent.Object);\n\n services.AddSingleton(mockModelResolver.Object);\n services.AddSingleton(mockAgentFactory.Object);\n services.AddSingleton<UnifiedAgent>();\n\n var serviceProvider = services.BuildServiceProvider();\n var unifiedAgent = serviceProvider.GetRequiredService<UnifiedAgent>();\n\n var messages = new List<IMessage>\n {\n new TextMessage { Role = Role.User, Text = \"Hello, world!\" },\n };\n\n // Pass null options but the agent should still work by getting model from context\n // We need to test this differently since we can't pass null options and expect it to work\n // Let's test with minimal options instead\n var minimalOptions = new GenerateReplyOptions { ModelId = \"deepseek-r1\" };\n\n // Act\n await unifiedAgent.GenerateReplyAsync(messages, minimalOptions);\n\n // Assert\n Assert.NotNull(capturedOptions);\n Assert.Equal(\"deepseek-reasoner\", capturedOptions.ModelId); // Should be updated to provider's model name\n\n // Verify the mock was called\n mockAgent.Verify(\n a =>\n a.GenerateReplyAsync(\n It.IsAny<IEnumerable<IMessage>>(),\n It.Is<GenerateReplyOptions>(o => o.ModelId == \"deepseek-reasoner\"),\n It.IsAny<CancellationToken>()\n ),\n Times.Once\n );\n }\n}\n"}, "6bccbb7295a72505de8077a73cb38b3c70abe0e0": {"t": 1785756737.3638895, "n": 16977, "h": "ce0d5512d51ca34b", "c": "using Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.Logging;\nusing Microsoft.Extensions.Options;\nusing Xunit;\nusing AchieveAi.LmDotnetTools.LmConfig.Agents;\nusing AchieveAi.LmDotnetTools.LmConfig.Models;\nusing AchieveAi.LmDotnetTools.LmConfig.Capabilities;\nusing AchieveAi.LmDotnetTools.LmCore.Messages;\nusing AchieveAi.LmDotnetTools.LmCore.Agents;\nusing Moq;\nusing System.Linq;\n\nnamespace LmConfig.Tests;\n\npublic class UnifiedAgentTests\n{\n private static ServiceCollection CreateServices(AppConfig config)\n {\n var services = new ServiceCollection();\n services.AddLogging();\n services.AddSingleton(Options.Create(config));\n return services;\n }\n\n private static async IAsyncEnumerable<IMessage> CreateEmptyAsyncEnumerable()\n {\n await Task.CompletedTask; // Make it async\n yield break; // Return empty sequence\n }\n\n [Fact]\n public void UnifiedAgent_ShouldImplementIAgent()\n {\n // Arrange\n var config = new AppConfig\n {\n Models = new[]\n {\n new ModelConfig\n {\n Id = \"test-model\",\n Capabilities = new ModelCapabilities \n { \n TokenLimits = new TokenLimits \n { \n MaxContextTokens = 4000,\n MaxOutputTokens = 1000,\n RecommendedMaxPromptTokens = 3000 \n } \n },\n Providers = new[]\n {\n new ProviderConfig\n {\n Name = \"TestProvider\",\n ModelName = \"test-model-name\",\n Priority = 1,\n Pricing = new PricingConfig { PromptPerMillion = 1.0, CompletionPerMillion = 2.0 },\n Tags = new[] { \"test\" }\n }\n }\n }\n },\n ProviderRegistry = new Dictionary<string, ProviderConnectionInfo>\n {\n [\"TestProvider\"] = new ProviderConnectionInfo\n {\n EndpointUrl = \"https://test.example.com\",\n ApiKeyEnvironmentVariable = \"TEST_API_KEY\",\n Compatibility = \"OpenAI\"\n }\n }\n };\n\n var services = CreateServices(config);\n services.AddSingleton<IModelResolver, ModelResolver>();\n services.AddSingleton<IProviderAgentFactory, ProviderAgentFactory>();\n services.AddSingleton<UnifiedAgent>();\n \n var serviceProvider = services.BuildServiceProvider();\n\n // Act\n var agent = serviceProvider.GetRequiredService<UnifiedAgent>();\n\n // Assert\n Assert.NotNull(agent);\n Assert.IsAssignableFrom<IAgent>(agent);\n Assert.IsAssignableFrom<IStreamingAgent>(agent);\n }\n\n [Fact]\n public async Task ModelResolver_ShouldResolveProvider()\n {\n // Arrange\n var config = new AppConfig\n {\n Models = new[]\n {\n new ModelConfig\n {\n Id = \"test-model\",\n Capabilities = new ModelCapabilities \n { \n TokenLimits = new TokenLimits \n { \n MaxContextTokens = 4000,\n MaxOutputTokens = 1000,\n RecommendedMaxPromptTokens = 3000 \n } \n },\n Providers = new[]\n {\n new ProviderConfig\n {\n Name = \"TestProvider\",\n ModelName = \"test-model-name\",\n Priority = 1,\n Pricing = new PricingConfig { PromptPerMillion = 1.0, CompletionPerMillion = 2.0 },\n Tags = new[] { \"test\" }\n }\n }\n }\n },\n ProviderRegistry = new Dictionary<string, ProviderConnectionInfo>\n {\n [\"TestProvider\"] = new ProviderConnectionInfo\n {\n EndpointUrl = \"https://test.example.com\",\n ApiKeyEnvironmentVariable = \"TEST_API_KEY\",\n Compatibility = \"OpenAI\"\n }\n }\n };\n\n // Set up the required environment variable for the test\n Environment.SetEnvironmentVariable(\"TEST_API_KEY\", \"test-api-key\");\n \n try\n {\n var services = CreateServices(config);\n services.AddSingleton<IModelResolver, ModelResolver>();\n \n var serviceProvider = services.BuildServiceProvider();\n var resolver = serviceProvider.GetRequiredService<IModelResolver>();\n\n // Act\n var resolution = await resolver.ResolveProviderAsync(\"test-model\");\n\n // Assert\n Assert.NotNull(resolution);\n Assert.Equal(\"test-model\", resolution.Model.Id);\n Assert.Equal(\"TestProvider\", resolution.Provider.Name);\n Assert.Equal(\"test-model-name\", resolution.Provider.ModelName);\n Assert.NotNull(resolution.Connection);\n Assert.Equal(\"https://test.example.com\", resolution.Connection.EndpointUrl);\n }\n finally\n {\n // Clean up the environment variable\n Environment.SetEnvironmentVariable(\"TEST_API_KEY\", null);\n }\n }\n\n [Fact]\n public async Task ModelResolver_ShouldReturnNullForUnknownModel()\n {\n // Arrange\n var config = new AppConfig\n {\n Models = Array.Empty<ModelConfig>(),\n ProviderRegistry = new Dictionary<string, ProviderConnectionInfo>()\n };\n\n var services = CreateServices(config);\n services.AddSingleton<IModelResolver, ModelResolver>();\n \n var serviceProvider = services.BuildServiceProvider();\n var resolver = serviceProvider.GetRequiredService<IModelResolver>();\n\n // Act\n var resolution = await resolver.ResolveProviderAsync(\"unknown-model\");\n\n // Assert\n Assert.Null(resolution);\n }\n\n [Fact]\n public async Task UnifiedAgent_ShouldUpdateModelIdWhenForwardingToProviderAgent()\n {\n // Arrange\n var config = new AppConfig\n {\n Models = new[]\n {\n new ModelConfig\n {\n Id = \"gpt-4.1\",\n Capabilities = new ModelCapabilities \n { \n TokenLimits = new TokenLimits \n { \n MaxContextTokens = 4000,\n MaxOutputTokens = 1000,\n RecommendedMaxPromptTokens = 3000 \n } \n },\n Providers = new[]\n {\n new ProviderConfig\n {\n Name = \"OpenRouter\",\n ModelName = \"openai/gpt-4.1\", // Different from the model ID\n Priority = 1,\n Pricing = new PricingConfig { PromptPerMillion = 2.0, CompletionPerMillion = 8.0 },\n Tags = new[] { \"openai-compatible\" }\n }\n }\n }\n },\n ProviderRegistry = new Dictionary<string, ProviderConnectionInfo>\n {\n [\"OpenRouter\"] = new ProviderConnectionInfo\n {\n EndpointUrl = \"https://openrouter.ai/api/v1\",\n ApiKeyEnvironmentVariable = \"OPENROUTER_API_KEY\",\n Compatibility = \"OpenAI\"\n }\n }\n };\n\n var services = CreateServices(config);\n \n // Create a mock model resolver that returns a predefined resolution\n var mockModelResolver = new Mock<IModelResolver>();\n var expectedResolution = new ProviderResolution\n {\n Model = config.Models.First(),\n Provider = config.Models.First().Providers.First(),\n Connection = config.ProviderRegistry[\"OpenRouter\"]\n };\n \n mockModelResolver.Setup(r => r.ResolveProviderAsync(\n It.IsAny<string>(), \n It.IsAny<ProviderSelectionCriteria>(), \n It.IsAny<CancellationToken>()))\n .ReturnsAsync(expectedResolution);\n \n // Create a mock agent factory that captures the options passed to it\n var mockAgentFactory = new Mock<IProviderAgentFactory>();\n var mockAgent = new Mock<IStreamingAgent>();\n \n GenerateReplyOptions? capturedOptions = null;\n \n mockAgent.Setup(a => a.GenerateReplyAsync(\n It.IsAny<IEnumerable<IMessage>>(), \n It.IsAny<GenerateReplyOptions>(), \n It.IsAny<CancellationToken>()))\n .Callback<IEnumerable<IMessage>, GenerateReplyOptions?, CancellationToken>(\n (messages, options, ct) => capturedOptions = options)\n .ReturnsAsync(new List<IMessage> { new TextMessage { Role = Role.Assistant, Text = \"Test response\" } });\n\n mockAgentFactory.Setup(f => f.CreateAgent(It.IsAny<ProviderResolution>()))\n .Returns(mockAgent.Object);\n\n services.AddSingleton(mockModelResolver.Object);\n services.AddSingleton(mockAgentFactory.Object);\n services.AddSingleton<UnifiedAgent>();\n \n var serviceProvider = services.BuildServiceProvider();\n var unifiedAgent = serviceProvider.GetRequiredService<UnifiedAgent>();\n\n var messages = new List<IMessage>\n {\n new TextMessage { Role = Role.User, Text = \"Hello, world!\" }\n };\n\n var originalOptions = new GenerateReplyOptions\n {\n ModelId = \"gpt-4.1\", // User specifies the logical model ID\n Temperature = 0.7f\n };\n\n // Act\n await unifiedAgent.GenerateReplyAsync(messages, originalOptions);\n\n // Assert\n Assert.NotNull(capturedOptions);\n Assert.Equal(\"openai/gpt-4.1\", capturedOptions.ModelId); // Should be updated to provider's model name\n Assert.Equal(0.7f, capturedOptions.Temperature); // Other properties should be preserved\n \n // Verify the mock was called\n mockAgent.Verify(a => a.GenerateReplyAsync(\n It.IsAny<IEnumerable<IMessage>>(), \n It.Is<GenerateReplyOptions>(o => o.ModelId == \"openai/gpt-4.1\"), \n It.IsAny<CancellationToken>()), Times.Once);\n }\n\n [Fact]\n public async Task UnifiedAgent_ShouldUpdateModelIdForStreamingRequests()\n {\n // Arrange\n var config = new AppConfig\n {\n Models = new[]\n {\n new ModelConfig\n {\n Id = \"claude-3-sonnet\",\n Capabilities = new ModelCapabilities \n { \n TokenLimits = new TokenLimits \n { \n MaxContextTokens = 4000,\n MaxOutputTokens = 1000,\n RecommendedMaxPromptTokens = 3000 \n } \n },\n Providers = new[]\n {\n new ProviderConfig\n {\n Name = \"OpenRouter\",\n ModelName = \"anthropic/claude-3-sonnet\", // Different from the model ID\n Priority = 1,\n Pricing = new PricingConfig { PromptPerMillion = 3.0, CompletionPerMillion = 15.0 },\n Tags = new[] { \"openai-compatible\" }\n }\n }\n }\n },\n ProviderRegistry = new Dictionary<string, ProviderConnectionInfo>\n {\n [\"OpenRouter\"] = new ProviderConnectionInfo\n {\n EndpointUrl = \"https://openrouter.ai/api/v1\",\n ApiKeyEnvironmentVariable = \"OPENROUTER_API_KEY\",\n Compatibility = \"OpenAI\"\n }\n }\n };\n\n var services = CreateServices(config);\n \n // Create a mock model resolver that returns a predefined resolution\n var mockModelResolver = new Mock<IModelResolver>();\n var expectedResolution = new ProviderResolution\n {\n Model = config.Models.First(),\n Provider = config.Models.First().Providers.First(),\n Connection = config.ProviderRegistry[\"OpenRouter\"]\n };\n \n mockModelResolver.Setup(r => r.ResolveProviderAsync(\n It.IsAny<string>(), \n It.IsAny<ProviderSelectionCriteria>(), \n It.IsAny<CancellationToken>()))\n .ReturnsAsync(expectedResolution);\n \n // Create a mock agent factory that captures the options passed to it\n var mockAgentFactory = new Mock<IProviderAgentFactory>();\n var mockAgent = new Mock<IStreamingAgent>();\n \n GenerateReplyOptions? capturedOptions = null;\n \n mockAgent.Setup(a => a.GenerateReplyStreamingAsync(\n It.IsAny<IEnumerable<IMessage>>(), \n It.IsAny<GenerateReplyOptions>(), \n It.IsAny<CancellationToken>()))\n .Callback<IEnumerable<IMessage>, GenerateReplyOptions?, CancellationToken>(\n (messages, options, ct) => capturedOptions = options)\n .ReturnsAsync(CreateEmptyAsyncEnumerable());\n\n mockAgentFactory.Setup(f => f.CreateStreamingAgent(It.IsAny<ProviderResolution>()))\n .Returns(mockAgent.Object);\n\n services.AddSingleton(mockModelResolver.Object);\n services.AddSingleton(mockAgentFactory.Object);\n services.AddSingleton<UnifiedAgent>();\n \n var serviceProvider = services.BuildServiceProvider();\n var unifiedAgent = serviceProvider.GetRequiredService<UnifiedAgent>();\n\n var messages = new List<IMessage>\n {\n new TextMessage { Role = Role.User, Text = \"Hello, world!\" }\n };\n\n var originalOptions = new GenerateReplyOptions\n {\n ModelId = \"claude-3-sonnet\", // User specifies the logical model ID\n Temperature = 0.5f,\n MaxToken = 2000\n };\n\n // Act\n await unifiedAgent.GenerateReplyStreamingAsync(messages, originalOptions);\n\n // Assert\n Assert.NotNull(capturedOptions);\n Assert.Equal(\"anthropic/claude-3-sonnet\", capturedOptions.ModelId); // Should be updated to provider's model name\n Assert.Equal(0.5f, capturedOptions.Temperature); // Other properties should be preserved\n Assert.Equal(2000, capturedOptions.MaxToken); // Other properties should be preserved\n \n // Verify the mock was called\n mockAgent.Verify(a => a.GenerateReplyStreamingAsync(\n It.IsAny<IEnumerable<IMessage>>(), \n It.Is<GenerateReplyOptions>(o => o.ModelId == \"anthropic/claude-3-sonnet\"), \n It.IsAny<CancellationToken>()), Times.Once);\n }\n\n [Fact]\n public async Task UnifiedAgent_ShouldCreateOptionsWithEffectiveModelNameWhenNullOptions()\n {\n // Arrange\n var config = new AppConfig\n {\n Models = new[]\n {\n new ModelConfig\n {\n Id = \"deepseek-r1\",\n Capabilities = new ModelCapabilities \n { \n TokenLimits = new TokenLimits \n { \n MaxContextTokens = 4000,\n MaxOutputTokens = 1000,\n RecommendedMaxPromptTokens = 3000 \n } \n },\n Providers = new[]\n {\n new ProviderConfig\n {\n Name = \"DeepSeek\",\n ModelName = \"deepseek-reasoner\", // Different from the model ID\n Priority = 1,\n Pricing = new PricingConfig { PromptPerMillion = 0.55, CompletionPerMillion = 2.19 },\n Tags = new[] { \"reasoning\" }\n }\n }\n }\n },\n ProviderRegistry = new Dictionary<string, ProviderConnectionInfo>\n {\n [\"DeepSeek\"] = new ProviderConnectionInfo\n {\n EndpointUrl = \"https://api.deepseek.com/v1\",\n ApiKeyEnvironmentVariable = \"DEEPSEEK_API_KEY\",\n Compatibility = \"OpenAI\"\n }\n }\n };\n\n var services = CreateServices(config);\n \n // Create a mock model resolver that returns a predefined resolution\n var mockModelResolver = new Mock<IModelResolver>();\n var expectedResolution = new ProviderResolution\n {\n Model = config.Models.First(),\n Provider = config.Models.First().Providers.First(),\n Connection = config.ProviderRegistry[\"DeepSeek\"]\n };\n \n mockModelResolver.Setup(r => r.ResolveProviderAsync(\n It.IsAny<string>(), \n It.IsAny<ProviderSelectionCriteria>(), \n It.IsAny<CancellationToken>()))\n .ReturnsAsync(expectedResolution);\n \n // Create a mock agent factory that captures the options passed to it\n var mockAgentFactory = new Mock<IProviderAgentFactory>();\n var mockAgent = new Mock<IStreamingAgent>();\n \n GenerateReplyOptions? capturedOptions = null;\n \n mockAgent.Setup(a => a.GenerateReplyAsync(\n It.IsAny<IEnumerable<IMessage>>(), \n It.IsAny<GenerateReplyOptions>(), \n It.IsAny<CancellationToken>()))\n .Callback<IEnumerable<IMessage>, GenerateReplyOptions?, CancellationToken>(\n (messages, options, ct) => capturedOptions = options)\n .ReturnsAsync(new List<IMessage> { new TextMessage { Role = Role.Assistant, Text = \"Test response\" } });\n\n mockAgentFactory.Setup(f => f.CreateAgent(It.IsAny<ProviderResolution>()))\n .Returns(mockAgent.Object);\n\n services.AddSingleton(mockModelResolver.Object);\n services.AddSingleton(mockAgentFactory.Object);\n services.AddSingleton<UnifiedAgent>();\n \n var serviceProvider = services.BuildServiceProvider();\n var unifiedAgent = serviceProvider.GetRequiredService<UnifiedAgent>();\n\n var messages = new List<IMessage>\n {\n new TextMessage { Role = Role.User, Text = \"Hello, world!\" }\n };\n\n // Pass null options but the agent should still work by getting model from context\n // We need to test this differently since we can't pass null options and expect it to work\n // Let's test with minimal options instead\n var minimalOptions = new GenerateReplyOptions\n {\n ModelId = \"deepseek-r1\"\n };\n\n // Act\n await unifiedAgent.GenerateReplyAsync(messages, minimalOptions);\n\n // Assert\n Assert.NotNull(capturedOptions);\n Assert.Equal(\"deepseek-reasoner\", capturedOptions.ModelId); // Should be updated to provider's model name\n \n // Verify the mock was called\n mockAgent.Verify(a => a.GenerateReplyAsync(\n It.IsAny<IEnumerable<IMessage>>(), \n It.Is<GenerateReplyOptions>(o => o.ModelId == \"deepseek-reasoner\"), \n It.IsAny<CancellationToken>()), Times.Once);\n }\n} "}, "ddd4d029a734677e4b28624bd4663e1143d96e86": {"t": 1786067477.712407, "n": 20653, "h": "2ac87503efdbef2b", "c": "using AchieveAi.LmDotnetTools.LmConfig.Agents;\nusing AchieveAi.LmDotnetTools.LmConfig.Capabilities;\nusing AchieveAi.LmDotnetTools.LmConfig.Models;\nusing AchieveAi.LmDotnetTools.LmCore.Agents;\nusing AchieveAi.LmDotnetTools.LmCore.Core;\nusing AchieveAi.LmDotnetTools.LmCore.Messages;\nusing Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.Options;\nusing Moq;\n\nnamespace LmConfig.Tests;\n\npublic class UnifiedAgentTests\n{\n private static ServiceCollection CreateServices(AppConfig config)\n {\n var services = new ServiceCollection();\n services.AddLogging();\n services.AddSingleton(Options.Create(config));\n services.AddSingleton<\n AchieveAi.LmDotnetTools.LmConfig.Http.IHttpHandlerBuilder,\n AchieveAi.LmDotnetTools.LmConfig.Http.HandlerBuilder>();\n return services;\n }\n\n private static async IAsyncEnumerable<IMessage> CreateEmptyAsyncEnumerable()\n {\n await Task.CompletedTask; // Make it async\n yield break; // Return empty sequence\n }\n\n [Fact]\n public void UnifiedAgent_ShouldImplementIAgent()\n {\n // Arrange\n var config = new AppConfig\n {\n Models =\n [\n new ModelConfig\n {\n Id = \"test-model\",\n Capabilities = new ModelCapabilities\n {\n TokenLimits = new TokenLimits\n {\n MaxContextTokens = 4000,\n MaxOutputTokens = 1000,\n RecommendedMaxPromptTokens = 3000,\n },\n },\n Providers =\n [\n new ProviderConfig\n {\n Name = \"TestProvider\",\n ModelName = \"test-model-name\",\n Priority = 1,\n Pricing = new PricingConfig { PromptPerMillion = 1.0, CompletionPerMillion = 2.0 },\n Tags = [\"test\"],\n },\n ],\n },\n ],\n ProviderRegistry = new Dictionary<string, ProviderConnectionInfo>\n {\n [\"TestProvider\"] = new ProviderConnectionInfo\n {\n EndpointUrl = \"https://test.example.com\",\n ApiKeyEnvironmentVariable = \"TEST_API_KEY\",\n Compatibility = \"OpenAI\",\n },\n },\n };\n\n var services = CreateServices(config);\n services.AddSingleton<IModelResolver, ModelResolver>();\n services.AddSingleton<IProviderAgentFactory, ProviderAgentFactory>();\n services.AddSingleton<UnifiedAgent>();\n\n var serviceProvider = services.BuildServiceProvider();\n\n // Act\n var agent = serviceProvider.GetRequiredService<UnifiedAgent>();\n\n // Assert\n Assert.NotNull(agent);\n Assert.IsAssignableFrom<IAgent>(agent);\n Assert.IsAssignableFrom<IStreamingAgent>(agent);\n }\n\n [Fact]\n public async Task ModelResolver_ShouldResolveProvider()\n {\n // Arrange\n var config = new AppConfig\n {\n Models =\n [\n new ModelConfig\n {\n Id = \"test-model\",\n Capabilities = new ModelCapabilities\n {\n TokenLimits = new TokenLimits\n {\n MaxContextTokens = 4000,\n MaxOutputTokens = 1000,\n RecommendedMaxPromptTokens = 3000,\n },\n },\n Providers =\n [\n new ProviderConfig\n {\n Name = \"TestProvider\",\n ModelName = \"test-model-name\",\n Priority = 1,\n Pricing = new PricingConfig { PromptPerMillion = 1.0, CompletionPerMillion = 2.0 },\n Tags = [\"test\"],\n },\n ],\n },\n ],\n ProviderRegistry = new Dictionary<string, ProviderConnectionInfo>\n {\n [\"TestProvider\"] = new ProviderConnectionInfo\n {\n EndpointUrl = \"https://test.example.com\",\n ApiKeyEnvironmentVariable = \"TEST_API_KEY\",\n Compatibility = \"OpenAI\",\n },\n },\n };\n\n // Set up the required environment variable for the test\n Environment.SetEnvironmentVariable(\"TEST_API_KEY\", \"test-api-key\");\n\n try\n {\n var services = CreateServices(config);\n services.AddSingleton<IModelResolver, ModelResolver>();\n\n var serviceProvider = services.BuildServiceProvider();\n var resolver = serviceProvider.GetRequiredService<IModelResolver>();\n\n // Act\n var resolution = await resolver.ResolveProviderAsync(\"test-model\");\n\n // Assert\n Assert.NotNull(resolution);\n Assert.Equal(\"test-model\", resolution.Model.Id);\n Assert.Equal(\"TestProvider\", resolution.Provider.Name);\n Assert.Equal(\"test-model-name\", resolution.Provider.ModelName);\n Assert.NotNull(resolution.Connection);\n Assert.Equal(\"https://test.example.com\", resolution.Connection.EndpointUrl);\n }\n finally\n {\n // Clean up the environment variable\n Environment.SetEnvironmentVariable(\"TEST_API_KEY\", null);\n }\n }\n\n [Fact]\n public async Task ModelResolver_ShouldReturnNullForUnknownModel()\n {\n // Arrange\n var config = new AppConfig\n {\n Models = [],\n ProviderRegistry = new Dictionary<string, ProviderConnectionInfo>(),\n };\n\n var services = CreateServices(config);\n services.AddSingleton<IModelResolver, ModelResolver>();\n\n var serviceProvider = services.BuildServiceProvider();\n var resolver = serviceProvider.GetRequiredService<IModelResolver>();\n\n // Act\n var resolution = await resolver.ResolveProviderAsync(\"unknown-model\");\n\n // Assert\n Assert.Null(resolution);\n }\n\n [Fact]\n public async Task UnifiedAgent_ShouldUpdateModelIdWhenForwardingToProviderAgent()\n {\n // Arrange\n var config = new AppConfig\n {\n Models =\n [\n new ModelConfig\n {\n Id = \"gpt-4.1\",\n Capabilities = new ModelCapabilities\n {\n TokenLimits = new TokenLimits\n {\n MaxContextTokens = 4000,\n MaxOutputTokens = 1000,\n RecommendedMaxPromptTokens = 3000,\n },\n },\n Providers =\n [\n new ProviderConfig\n {\n Name = \"OpenRouter\",\n ModelName = \"openai/gpt-4.1\", // Different from the model ID\n Priority = 1,\n Pricing = new PricingConfig { PromptPerMillion = 2.0, CompletionPerMillion = 8.0 },\n Tags = [\"openai-compatible\"],\n },\n ],\n },\n ],\n ProviderRegistry = new Dictionary<string, ProviderConnectionInfo>\n {\n [\"OpenRouter\"] = new ProviderConnectionInfo\n {\n EndpointUrl = \"https://openrouter.ai/api/v1\",\n ApiKeyEnvironmentVariable = \"OPENROUTER_API_KEY\",\n Compatibility = \"OpenAI\",\n },\n },\n };\n\n var services = CreateServices(config);\n\n // Create a mock model resolver that returns a predefined resolution\n var mockModelResolver = new Mock<IModelResolver>();\n var expectedResolution = new ProviderResolution\n {\n Model = config.Models[0],\n Provider = config.Models[0].Providers[0],\n Connection = config.ProviderRegistry[\"OpenRouter\"],\n };\n\n mockModelResolver\n .Setup(r =>\n r.ResolveProviderAsync(\n It.IsAny<string>(),\n It.IsAny<ProviderSelectionCriteria>(),\n It.IsAny<CancellationToken>()\n )\n )\n .ReturnsAsync(expectedResolution);\n\n // Create a mock agent factory that captures the options passed to it\n var mockAgentFactory = new Mock<IProviderAgentFactory>();\n var mockAgent = new Mock<IStreamingAgent>();\n\n GenerateReplyOptions? capturedOptions = null;\n\n mockAgent\n .Setup(a =>\n a.GenerateReplyAsync(\n It.IsAny<IEnumerable<IMessage>>(),\n It.IsAny<GenerateReplyOptions>(),\n It.IsAny<CancellationToken>()\n )\n )\n .Callback<IEnumerable<IMessage>, GenerateReplyOptions?, CancellationToken>(\n (messages, options, ct) => capturedOptions = options\n )\n .ReturnsAsync(\n [\n new TextMessage { Role = Role.Assistant, Text = \"Test response\" },\n ]\n );\n\n mockAgentFactory.Setup(f => f.CreateAgent(It.IsAny<ProviderResolution>())).Returns(mockAgent.Object);\n\n services.AddSingleton(mockModelResolver.Object);\n services.AddSingleton(mockAgentFactory.Object);\n services.AddSingleton<UnifiedAgent>();\n\n var serviceProvider = services.BuildServiceProvider();\n var unifiedAgent = serviceProvider.GetRequiredService<UnifiedAgent>();\n\n var messages = new List<IMessage>\n {\n new TextMessage { Role = Role.User, Text = \"Hello, world!\" },\n };\n\n var originalOptions = new GenerateReplyOptions\n {\n ModelId = \"gpt-4.1\", // User specifies the logical model ID\n Temperature = 0.7f,\n };\n\n // Act\n await unifiedAgent.GenerateReplyAsync(messages, originalOptions);\n\n // Assert\n Assert.NotNull(capturedOptions);\n Assert.Equal(\"openai/gpt-4.1\", capturedOptions.ModelId); // Should be updated to provider's model name\n Assert.Equal(0.7f, capturedOptions.Temperature); // Other properties should be preserved\n\n // Verify the mock was called\n mockAgent.Verify(\n a =>\n a.GenerateReplyAsync(\n It.IsAny<IEnumerable<IMessage>>(),\n It.Is<GenerateReplyOptions>(o => o.ModelId == \"openai/gpt-4.1\"),\n It.IsAny<CancellationToken>()\n ),\n Times.Once\n );\n }\n\n [Fact]\n public async Task UnifiedAgent_ShouldUpdateModelIdForStreamingRequests()\n {\n // Arrange\n var config = new AppConfig\n {\n Models =\n [\n new ModelConfig\n {\n Id = \"claude-3-sonnet\",\n Capabilities = new ModelCapabilities\n {\n TokenLimits = new TokenLimits\n {\n MaxContextTokens = 4000,\n MaxOutputTokens = 1000,\n RecommendedMaxPromptTokens = 3000,\n },\n },\n Providers =\n [\n new ProviderConfig\n {\n Name = \"OpenRouter\",\n ModelName = \"anthropic/claude-3-sonnet\", // Different from the model ID\n Priority = 1,\n Pricing = new PricingConfig { PromptPerMillion = 3.0, CompletionPerMillion = 15.0 },\n Tags = [\"openai-compatible\"],\n },\n ],\n },\n ],\n ProviderRegistry = new Dictionary<string, ProviderConnectionInfo>\n {\n [\"OpenRouter\"] = new ProviderConnectionInfo\n {\n EndpointUrl = \"https://openrouter.ai/api/v1\",\n ApiKeyEnvironmentVariable = \"OPENROUTER_API_KEY\",\n Compatibility = \"OpenAI\",\n },\n },\n };\n\n var services = CreateServices(config);\n\n // Create a mock model resolver that returns a predefined resolution\n var mockModelResolver = new Mock<IModelResolver>();\n var expectedResolution = new ProviderResolution\n {\n Model = config.Models[0],\n Provider = config.Models[0].Providers[0],\n Connection = config.ProviderRegistry[\"OpenRouter\"],\n };\n\n mockModelResolver\n .Setup(r =>\n r.ResolveProviderAsync(\n It.IsAny<string>(),\n It.IsAny<ProviderSelectionCriteria>(),\n It.IsAny<CancellationToken>()\n )\n )\n .ReturnsAsync(expectedResolution);\n\n // Create a mock agent factory that captures the options passed to it\n var mockAgentFactory = new Mock<IProviderAgentFactory>();\n var mockAgent = new Mock<IStreamingAgent>();\n\n GenerateReplyOptions? capturedOptions = null;\n\n mockAgent\n .Setup(a =>\n a.GenerateReplyStreamingAsync(\n It.IsAny<IEnumerable<IMessage>>(),\n It.IsAny<GenerateReplyOptions>(),\n It.IsAny<CancellationToken>()\n )\n )\n .Callback<IEnumerable<IMessage>, GenerateReplyOptions?, CancellationToken>(\n (messages, options, ct) => capturedOptions = options\n )\n .ReturnsAsync(CreateEmptyAsyncEnumerable());\n\n mockAgentFactory.Setup(f => f.CreateStreamingAgent(It.IsAny<ProviderResolution>())).Returns(mockAgent.Object);\n\n services.AddSingleton(mockModelResolver.Object);\n services.AddSingleton(mockAgentFactory.Object);\n services.AddSingleton<UnifiedAgent>();\n\n var serviceProvider = services.BuildServiceProvider();\n var unifiedAgent = serviceProvider.GetRequiredService<UnifiedAgent>();\n\n var messages = new List<IMessage>\n {\n new TextMessage { Role = Role.User, Text = \"Hello, world!\" },\n };\n\n var originalOptions = new GenerateReplyOptions\n {\n ModelId = \"claude-3-sonnet\", // User specifies the logical model ID\n Temperature = 0.5f,\n MaxToken = 2000,\n };\n\n // Act\n await unifiedAgent.GenerateReplyStreamingAsync(messages, originalOptions);\n\n // Assert\n Assert.NotNull(capturedOptions);\n Assert.Equal(\"anthropic/claude-3-sonnet\", capturedOptions.ModelId); // Should be updated to provider's model name\n Assert.Equal(0.5f, capturedOptions.Temperature); // Other properties should be preserved\n Assert.Equal(2000, capturedOptions.MaxToken); // Other properties should be preserved\n\n // Verify the mock was called\n mockAgent.Verify(\n a =>\n a.GenerateReplyStreamingAsync(\n It.IsAny<IEnumerable<IMessage>>(),\n It.Is<GenerateReplyOptions>(o => o.ModelId == \"anthropic/claude-3-sonnet\"),\n It.IsAny<CancellationToken>()\n ),\n Times.Once\n );\n }\n\n [Fact]\n public async Task UnifiedAgent_ShouldCreateOptionsWithEffectiveModelNameWhenNullOptions()\n {\n // Arrange\n var config = new AppConfig\n {\n Models =\n [\n new ModelConfig\n {\n Id = \"deepseek-r1\",\n Capabilities = new ModelCapabilities\n {\n TokenLimits = new TokenLimits\n {\n MaxContextTokens = 4000,\n MaxOutputTokens = 1000,\n RecommendedMaxPromptTokens = 3000,\n },\n },\n Providers =\n [\n new ProviderConfig\n {\n Name = \"DeepSeek\",\n ModelName = \"deepseek-reasoner\", // Different from the model ID\n Priority = 1,\n Pricing = new PricingConfig { PromptPerMillion = 0.55, CompletionPerMillion = 2.19 },\n Tags = [\"reasoning\"],\n },\n ],\n },\n ],\n ProviderRegistry = new Dictionary<string, ProviderConnectionInfo>\n {\n [\"DeepSeek\"] = new ProviderConnectionInfo\n {\n EndpointUrl = \"https://api.deepseek.com/v1\",\n ApiKeyEnvironmentVariable = \"DEEPSEEK_API_KEY\",\n Compatibility = \"OpenAI\",\n },\n },\n };\n\n var services = CreateServices(config);\n\n // Create a mock model resolver that returns a predefined resolution\n var mockModelResolver = new Mock<IModelResolver>();\n var expectedResolution = new ProviderResolution\n {\n Model = config.Models[0],\n Provider = config.Models[0].Providers[0],\n Connection = config.ProviderRegistry[\"DeepSeek\"],\n };\n\n mockModelResolver\n .Setup(r =>\n r.ResolveProviderAsync(\n It.IsAny<string>(),\n It.IsAny<ProviderSelectionCriteria>(),\n It.IsAny<CancellationToken>()\n )\n )\n .ReturnsAsync(expectedResolution);\n\n // Create a mock agent factory that captures the options passed to it\n var mockAgentFactory = new Mock<IProviderAgentFactory>();\n var mockAgent = new Mock<IStreamingAgent>();\n\n GenerateReplyOptions? capturedOptions = null;\n\n mockAgent\n .Setup(a =>\n a.GenerateReplyAsync(\n It.IsAny<IEnumerable<IMessage>>(),\n It.IsAny<GenerateReplyOptions>(),\n It.IsAny<CancellationToken>()\n )\n )\n .Callback<IEnumerable<IMessage>, GenerateReplyOptions?, CancellationToken>(\n (messages, options, ct) => capturedOptions = options\n )\n .ReturnsAsync(\n [\n new TextMessage { Role = Role.Assistant, Text = \"Test response\" },\n ]\n );\n\n mockAgentFactory.Setup(f => f.CreateAgent(It.IsAny<ProviderResolution>())).Returns(mockAgent.Object);\n\n services.AddSingleton(mockModelResolver.Object);\n services.AddSingleton(mockAgentFactory.Object);\n services.AddSingleton<UnifiedAgent>();\n\n var serviceProvider = services.BuildServiceProvider();\n var unifiedAgent = serviceProvider.GetRequiredService<UnifiedAgent>();\n\n var messages = new List<IMessage>\n {\n new TextMessage { Role = Role.User, Text = \"Hello, world!\" },\n };\n\n // Pass null options but the agent should still work by getting model from context\n // We need to test this differently since we can't pass null options and expect it to work\n // Let's test with minimal options instead\n var minimalOptions = new GenerateReplyOptions { ModelId = \"deepseek-r1\" };\n\n // Act\n await unifiedAgent.GenerateReplyAsync(messages, minimalOptions);\n\n // Assert\n Assert.NotNull(capturedOptions);\n Assert.Equal(\"deepseek-reasoner\", capturedOptions.ModelId); // Should be updated to provider's model name\n\n // Verify the mock was called\n mockAgent.Verify(\n a =>\n a.GenerateReplyAsync(\n It.IsAny<IEnumerable<IMessage>>(),\n It.Is<GenerateReplyOptions>(o => o.ModelId == \"deepseek-reasoner\"),\n It.IsAny<CancellationToken>()\n ),\n Times.Once\n );\n }\n}\n"}, "3a25297b94f73fcda85e6e48bc8b48c2ee8e662d": {"t": 1786068324.5936968, "n": 20653, "h": "2ac87503efdbef2b", "c": "using AchieveAi.LmDotnetTools.LmConfig.Agents;\nusing AchieveAi.LmDotnetTools.LmConfig.Capabilities;\nusing AchieveAi.LmDotnetTools.LmConfig.Models;\nusing AchieveAi.LmDotnetTools.LmCore.Agents;\nusing AchieveAi.LmDotnetTools.LmCore.Core;\nusing AchieveAi.LmDotnetTools.LmCore.Messages;\nusing Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.Options;\nusing Moq;\n\nnamespace LmConfig.Tests;\n\npublic class UnifiedAgentTests\n{\n private static ServiceCollection CreateServices(AppConfig config)\n {\n var services = new ServiceCollection();\n services.AddLogging();\n services.AddSingleton(Options.Create(config));\n services.AddSingleton<\n AchieveAi.LmDotnetTools.LmConfig.Http.IHttpHandlerBuilder,\n AchieveAi.LmDotnetTools.LmConfig.Http.HandlerBuilder>();\n return services;\n }\n\n private static async IAsyncEnumerable<IMessage> CreateEmptyAsyncEnumerable()\n {\n await Task.CompletedTask; // Make it async\n yield break; // Return empty sequence\n }\n\n [Fact]\n public void UnifiedAgent_ShouldImplementIAgent()\n {\n // Arrange\n var config = new AppConfig\n {\n Models =\n [\n new ModelConfig\n {\n Id = \"test-model\",\n Capabilities = new ModelCapabilities\n {\n TokenLimits = new TokenLimits\n {\n MaxContextTokens = 4000,\n MaxOutputTokens = 1000,\n RecommendedMaxPromptTokens = 3000,\n },\n },\n Providers =\n [\n new ProviderConfig\n {\n Name = \"TestProvider\",\n ModelName = \"test-model-name\",\n Priority = 1,\n Pricing = new PricingConfig { PromptPerMillion = 1.0, CompletionPerMillion = 2.0 },\n Tags = [\"test\"],\n },\n ],\n },\n ],\n ProviderRegistry = new Dictionary<string, ProviderConnectionInfo>\n {\n [\"TestProvider\"] = new ProviderConnectionInfo\n {\n EndpointUrl = \"https://test.example.com\",\n ApiKeyEnvironmentVariable = \"TEST_API_KEY\",\n Compatibility = \"OpenAI\",\n },\n },\n };\n\n var services = CreateServices(config);\n services.AddSingleton<IModelResolver, ModelResolver>();\n services.AddSingleton<IProviderAgentFactory, ProviderAgentFactory>();\n services.AddSingleton<UnifiedAgent>();\n\n var serviceProvider = services.BuildServiceProvider();\n\n // Act\n var agent = serviceProvider.GetRequiredService<UnifiedAgent>();\n\n // Assert\n Assert.NotNull(agent);\n Assert.IsAssignableFrom<IAgent>(agent);\n Assert.IsAssignableFrom<IStreamingAgent>(agent);\n }\n\n [Fact]\n public async Task ModelResolver_ShouldResolveProvider()\n {\n // Arrange\n var config = new AppConfig\n {\n Models =\n [\n new ModelConfig\n {\n Id = \"test-model\",\n Capabilities = new ModelCapabilities\n {\n TokenLimits = new TokenLimits\n {\n MaxContextTokens = 4000,\n MaxOutputTokens = 1000,\n RecommendedMaxPromptTokens = 3000,\n },\n },\n Providers =\n [\n new ProviderConfig\n {\n Name = \"TestProvider\",\n ModelName = \"test-model-name\",\n Priority = 1,\n Pricing = new PricingConfig { PromptPerMillion = 1.0, CompletionPerMillion = 2.0 },\n Tags = [\"test\"],\n },\n ],\n },\n ],\n ProviderRegistry = new Dictionary<string, ProviderConnectionInfo>\n {\n [\"TestProvider\"] = new ProviderConnectionInfo\n {\n EndpointUrl = \"https://test.example.com\",\n ApiKeyEnvironmentVariable = \"TEST_API_KEY\",\n Compatibility = \"OpenAI\",\n },\n },\n };\n\n // Set up the required environment variable for the test\n Environment.SetEnvironmentVariable(\"TEST_API_KEY\", \"test-api-key\");\n\n try\n {\n var services = CreateServices(config);\n services.AddSingleton<IModelResolver, ModelResolver>();\n\n var serviceProvider = services.BuildServiceProvider();\n var resolver = serviceProvider.GetRequiredService<IModelResolver>();\n\n // Act\n var resolution = await resolver.ResolveProviderAsync(\"test-model\");\n\n // Assert\n Assert.NotNull(resolution);\n Assert.Equal(\"test-model\", resolution.Model.Id);\n Assert.Equal(\"TestProvider\", resolution.Provider.Name);\n Assert.Equal(\"test-model-name\", resolution.Provider.ModelName);\n Assert.NotNull(resolution.Connection);\n Assert.Equal(\"https://test.example.com\", resolution.Connection.EndpointUrl);\n }\n finally\n {\n // Clean up the environment variable\n Environment.SetEnvironmentVariable(\"TEST_API_KEY\", null);\n }\n }\n\n [Fact]\n public async Task ModelResolver_ShouldReturnNullForUnknownModel()\n {\n // Arrange\n var config = new AppConfig\n {\n Models = [],\n ProviderRegistry = new Dictionary<string, ProviderConnectionInfo>(),\n };\n\n var services = CreateServices(config);\n services.AddSingleton<IModelResolver, ModelResolver>();\n\n var serviceProvider = services.BuildServiceProvider();\n var resolver = serviceProvider.GetRequiredService<IModelResolver>();\n\n // Act\n var resolution = await resolver.ResolveProviderAsync(\"unknown-model\");\n\n // Assert\n Assert.Null(resolution);\n }\n\n [Fact]\n public async Task UnifiedAgent_ShouldUpdateModelIdWhenForwardingToProviderAgent()\n {\n // Arrange\n var config = new AppConfig\n {\n Models =\n [\n new ModelConfig\n {\n Id = \"gpt-4.1\",\n Capabilities = new ModelCapabilities\n {\n TokenLimits = new TokenLimits\n {\n MaxContextTokens = 4000,\n MaxOutputTokens = 1000,\n RecommendedMaxPromptTokens = 3000,\n },\n },\n Providers =\n [\n new ProviderConfig\n {\n Name = \"OpenRouter\",\n ModelName = \"openai/gpt-4.1\", // Different from the model ID\n Priority = 1,\n Pricing = new PricingConfig { PromptPerMillion = 2.0, CompletionPerMillion = 8.0 },\n Tags = [\"openai-compatible\"],\n },\n ],\n },\n ],\n ProviderRegistry = new Dictionary<string, ProviderConnectionInfo>\n {\n [\"OpenRouter\"] = new ProviderConnectionInfo\n {\n EndpointUrl = \"https://openrouter.ai/api/v1\",\n ApiKeyEnvironmentVariable = \"OPENROUTER_API_KEY\",\n Compatibility = \"OpenAI\",\n },\n },\n };\n\n var services = CreateServices(config);\n\n // Create a mock model resolver that returns a predefined resolution\n var mockModelResolver = new Mock<IModelResolver>();\n var expectedResolution = new ProviderResolution\n {\n Model = config.Models[0],\n Provider = config.Models[0].Providers[0],\n Connection = config.ProviderRegistry[\"OpenRouter\"],\n };\n\n mockModelResolver\n .Setup(r =>\n r.ResolveProviderAsync(\n It.IsAny<string>(),\n It.IsAny<ProviderSelectionCriteria>(),\n It.IsAny<CancellationToken>()\n )\n )\n .ReturnsAsync(expectedResolution);\n\n // Create a mock agent factory that captures the options passed to it\n var mockAgentFactory = new Mock<IProviderAgentFactory>();\n var mockAgent = new Mock<IStreamingAgent>();\n\n GenerateReplyOptions? capturedOptions = null;\n\n mockAgent\n .Setup(a =>\n a.GenerateReplyAsync(\n It.IsAny<IEnumerable<IMessage>>(),\n It.IsAny<GenerateReplyOptions>(),\n It.IsAny<CancellationToken>()\n )\n )\n .Callback<IEnumerable<IMessage>, GenerateReplyOptions?, CancellationToken>(\n (messages, options, ct) => capturedOptions = options\n )\n .ReturnsAsync(\n [\n new TextMessage { Role = Role.Assistant, Text = \"Test response\" },\n ]\n );\n\n mockAgentFactory.Setup(f => f.CreateAgent(It.IsAny<ProviderResolution>())).Returns(mockAgent.Object);\n\n services.AddSingleton(mockModelResolver.Object);\n services.AddSingleton(mockAgentFactory.Object);\n services.AddSingleton<UnifiedAgent>();\n\n var serviceProvider = services.BuildServiceProvider();\n var unifiedAgent = serviceProvider.GetRequiredService<UnifiedAgent>();\n\n var messages = new List<IMessage>\n {\n new TextMessage { Role = Role.User, Text = \"Hello, world!\" },\n };\n\n var originalOptions = new GenerateReplyOptions\n {\n ModelId = \"gpt-4.1\", // User specifies the logical model ID\n Temperature = 0.7f,\n };\n\n // Act\n await unifiedAgent.GenerateReplyAsync(messages, originalOptions);\n\n // Assert\n Assert.NotNull(capturedOptions);\n Assert.Equal(\"openai/gpt-4.1\", capturedOptions.ModelId); // Should be updated to provider's model name\n Assert.Equal(0.7f, capturedOptions.Temperature); // Other properties should be preserved\n\n // Verify the mock was called\n mockAgent.Verify(\n a =>\n a.GenerateReplyAsync(\n It.IsAny<IEnumerable<IMessage>>(),\n It.Is<GenerateReplyOptions>(o => o.ModelId == \"openai/gpt-4.1\"),\n It.IsAny<CancellationToken>()\n ),\n Times.Once\n );\n }\n\n [Fact]\n public async Task UnifiedAgent_ShouldUpdateModelIdForStreamingRequests()\n {\n // Arrange\n var config = new AppConfig\n {\n Models =\n [\n new ModelConfig\n {\n Id = \"claude-3-sonnet\",\n Capabilities = new ModelCapabilities\n {\n TokenLimits = new TokenLimits\n {\n MaxContextTokens = 4000,\n MaxOutputTokens = 1000,\n RecommendedMaxPromptTokens = 3000,\n },\n },\n Providers =\n [\n new ProviderConfig\n {\n Name = \"OpenRouter\",\n ModelName = \"anthropic/claude-3-sonnet\", // Different from the model ID\n Priority = 1,\n Pricing = new PricingConfig { PromptPerMillion = 3.0, CompletionPerMillion = 15.0 },\n Tags = [\"openai-compatible\"],\n },\n ],\n },\n ],\n ProviderRegistry = new Dictionary<string, ProviderConnectionInfo>\n {\n [\"OpenRouter\"] = new ProviderConnectionInfo\n {\n EndpointUrl = \"https://openrouter.ai/api/v1\",\n ApiKeyEnvironmentVariable = \"OPENROUTER_API_KEY\",\n Compatibility = \"OpenAI\",\n },\n },\n };\n\n var services = CreateServices(config);\n\n // Create a mock model resolver that returns a predefined resolution\n var mockModelResolver = new Mock<IModelResolver>();\n var expectedResolution = new ProviderResolution\n {\n Model = config.Models[0],\n Provider = config.Models[0].Providers[0],\n Connection = config.ProviderRegistry[\"OpenRouter\"],\n };\n\n mockModelResolver\n .Setup(r =>\n r.ResolveProviderAsync(\n It.IsAny<string>(),\n It.IsAny<ProviderSelectionCriteria>(),\n It.IsAny<CancellationToken>()\n )\n )\n .ReturnsAsync(expectedResolution);\n\n // Create a mock agent factory that captures the options passed to it\n var mockAgentFactory = new Mock<IProviderAgentFactory>();\n var mockAgent = new Mock<IStreamingAgent>();\n\n GenerateReplyOptions? capturedOptions = null;\n\n mockAgent\n .Setup(a =>\n a.GenerateReplyStreamingAsync(\n It.IsAny<IEnumerable<IMessage>>(),\n It.IsAny<GenerateReplyOptions>(),\n It.IsAny<CancellationToken>()\n )\n )\n .Callback<IEnumerable<IMessage>, GenerateReplyOptions?, CancellationToken>(\n (messages, options, ct) => capturedOptions = options\n )\n .ReturnsAsync(CreateEmptyAsyncEnumerable());\n\n mockAgentFactory.Setup(f => f.CreateStreamingAgent(It.IsAny<ProviderResolution>())).Returns(mockAgent.Object);\n\n services.AddSingleton(mockModelResolver.Object);\n services.AddSingleton(mockAgentFactory.Object);\n services.AddSingleton<UnifiedAgent>();\n\n var serviceProvider = services.BuildServiceProvider();\n var unifiedAgent = serviceProvider.GetRequiredService<UnifiedAgent>();\n\n var messages = new List<IMessage>\n {\n new TextMessage { Role = Role.User, Text = \"Hello, world!\" },\n };\n\n var originalOptions = new GenerateReplyOptions\n {\n ModelId = \"claude-3-sonnet\", // User specifies the logical model ID\n Temperature = 0.5f,\n MaxToken = 2000,\n };\n\n // Act\n await unifiedAgent.GenerateReplyStreamingAsync(messages, originalOptions);\n\n // Assert\n Assert.NotNull(capturedOptions);\n Assert.Equal(\"anthropic/claude-3-sonnet\", capturedOptions.ModelId); // Should be updated to provider's model name\n Assert.Equal(0.5f, capturedOptions.Temperature); // Other properties should be preserved\n Assert.Equal(2000, capturedOptions.MaxToken); // Other properties should be preserved\n\n // Verify the mock was called\n mockAgent.Verify(\n a =>\n a.GenerateReplyStreamingAsync(\n It.IsAny<IEnumerable<IMessage>>(),\n It.Is<GenerateReplyOptions>(o => o.ModelId == \"anthropic/claude-3-sonnet\"),\n It.IsAny<CancellationToken>()\n ),\n Times.Once\n );\n }\n\n [Fact]\n public async Task UnifiedAgent_ShouldCreateOptionsWithEffectiveModelNameWhenNullOptions()\n {\n // Arrange\n var config = new AppConfig\n {\n Models =\n [\n new ModelConfig\n {\n Id = \"deepseek-r1\",\n Capabilities = new ModelCapabilities\n {\n TokenLimits = new TokenLimits\n {\n MaxContextTokens = 4000,\n MaxOutputTokens = 1000,\n RecommendedMaxPromptTokens = 3000,\n },\n },\n Providers =\n [\n new ProviderConfig\n {\n Name = \"DeepSeek\",\n ModelName = \"deepseek-reasoner\", // Different from the model ID\n Priority = 1,\n Pricing = new PricingConfig { PromptPerMillion = 0.55, CompletionPerMillion = 2.19 },\n Tags = [\"reasoning\"],\n },\n ],\n },\n ],\n ProviderRegistry = new Dictionary<string, ProviderConnectionInfo>\n {\n [\"DeepSeek\"] = new ProviderConnectionInfo\n {\n EndpointUrl = \"https://api.deepseek.com/v1\",\n ApiKeyEnvironmentVariable = \"DEEPSEEK_API_KEY\",\n Compatibility = \"OpenAI\",\n },\n },\n };\n\n var services = CreateServices(config);\n\n // Create a mock model resolver that returns a predefined resolution\n var mockModelResolver = new Mock<IModelResolver>();\n var expectedResolution = new ProviderResolution\n {\n Model = config.Models[0],\n Provider = config.Models[0].Providers[0],\n Connection = config.ProviderRegistry[\"DeepSeek\"],\n };\n\n mockModelResolver\n .Setup(r =>\n r.ResolveProviderAsync(\n It.IsAny<string>(),\n It.IsAny<ProviderSelectionCriteria>(),\n It.IsAny<CancellationToken>()\n )\n )\n .ReturnsAsync(expectedResolution);\n\n // Create a mock agent factory that captures the options passed to it\n var mockAgentFactory = new Mock<IProviderAgentFactory>();\n var mockAgent = new Mock<IStreamingAgent>();\n\n GenerateReplyOptions? capturedOptions = null;\n\n mockAgent\n .Setup(a =>\n a.GenerateReplyAsync(\n It.IsAny<IEnumerable<IMessage>>(),\n It.IsAny<GenerateReplyOptions>(),\n It.IsAny<CancellationToken>()\n )\n )\n .Callback<IEnumerable<IMessage>, GenerateReplyOptions?, CancellationToken>(\n (messages, options, ct) => capturedOptions = options\n )\n .ReturnsAsync(\n [\n new TextMessage { Role = Role.Assistant, Text = \"Test response\" },\n ]\n );\n\n mockAgentFactory.Setup(f => f.CreateAgent(It.IsAny<ProviderResolution>())).Returns(mockAgent.Object);\n\n services.AddSingleton(mockModelResolver.Object);\n services.AddSingleton(mockAgentFactory.Object);\n services.AddSingleton<UnifiedAgent>();\n\n var serviceProvider = services.BuildServiceProvider();\n var unifiedAgent = serviceProvider.GetRequiredService<UnifiedAgent>();\n\n var messages = new List<IMessage>\n {\n new TextMessage { Role = Role.User, Text = \"Hello, world!\" },\n };\n\n // Pass null options but the agent should still work by getting model from context\n // We need to test this differently since we can't pass null options and expect it to work\n // Let's test with minimal options instead\n var minimalOptions = new GenerateReplyOptions { ModelId = \"deepseek-r1\" };\n\n // Act\n await unifiedAgent.GenerateReplyAsync(messages, minimalOptions);\n\n // Assert\n Assert.NotNull(capturedOptions);\n Assert.Equal(\"deepseek-reasoner\", capturedOptions.ModelId); // Should be updated to provider's model name\n\n // Verify the mock was called\n mockAgent.Verify(\n a =>\n a.GenerateReplyAsync(\n It.IsAny<IEnumerable<IMessage>>(),\n It.Is<GenerateReplyOptions>(o => o.ModelId == \"deepseek-reasoner\"),\n It.IsAny<CancellationToken>()\n ),\n Times.Once\n );\n }\n}\n"}}, "tests/LmStreaming.Sample.Tests/Services/AnthropicCompatProvidersTests.cs": {"5df9a37e189f520689e30ebef085dd06c50d907c": {"t": 1785746195.086794, "n": 11524, "h": "4b39d91b2ec611a4", "c": "using LmStreaming.Sample.Services;\nusing Microsoft.Extensions.Logging;\n\nnamespace LmStreaming.Sample.Tests.Services;\n\n/// <summary>\n/// Tests for <see cref=\"AnthropicCompatProviders.DiscoverFromEnv\" /> — the env-var-driven discovery\n/// of generic Anthropic-compatible provider families (e.g. DeepSeek). Mirrors the \"degrade to empty\n/// list, never throw\" contract of the Copilot model discovery path.\n/// </summary>\n[Collection(\"EnvironmentVariables\")]\npublic class AnthropicCompatProvidersTests\n{\n [Fact]\n public void DiscoverFromEnv_ReturnsEmpty_WhenProvidersListUnset()\n {\n using var _ = EnvScope.Set(\"ANTHROPIC_COMPAT_PROVIDERS\", null);\n\n var models = AnthropicCompatProviders.DiscoverFromEnv(NullLoggerFactory.Instance);\n\n models.Should().BeEmpty();\n }\n\n [Fact]\n public void DiscoverFromEnv_ReturnsEmpty_WhenProvidersListBlank()\n {\n using var _ = EnvScope.Set(\"ANTHROPIC_COMPAT_PROVIDERS\", \" \");\n\n var models = AnthropicCompatProviders.DiscoverFromEnv(NullLoggerFactory.Instance);\n\n models.Should().BeEmpty();\n }\n\n [Fact]\n public void DiscoverFromEnv_ParsesSingleFamily_WithMultipleModels()\n {\n using var _ = EnvScope.Set(\"ANTHROPIC_COMPAT_PROVIDERS\", \"DEEPSEEK\");\n using var __ = EnvScope.Set(\"DEEPSEEK_ANTHROPIC_URL\", \"https://api.deepseek.com/anthropic\");\n using var ___ = EnvScope.Set(\"DEEPSEEK_APIKEY\", \"sk-deepseek\");\n using var ____ = EnvScope.Set(\"DEEPSEEK_MODELS\", \"deepseek-v4-pro,deepseek-v4-flash\");\n\n var models = AnthropicCompatProviders.DiscoverFromEnv(NullLoggerFactory.Instance);\n\n models.Should().BeEquivalentTo(\n [\n new AnthropicCompatModel(\n \"deepseek-v4-pro\", \"deepseek-v4-pro\", \"deepseek-v4-pro\",\n \"https://api.deepseek.com/anthropic\", \"sk-deepseek\", \"DEEPSEEK\"),\n new AnthropicCompatModel(\n \"deepseek-v4-flash\", \"deepseek-v4-flash\", \"deepseek-v4-flash\",\n \"https://api.deepseek.com/anthropic\", \"sk-deepseek\", \"DEEPSEEK\"),\n ],\n options => options.WithStrictOrdering());\n }\n\n [Fact]\n public void DiscoverFromEnv_ParsesMultipleFamilies()\n {\n using var _ = EnvScope.Set(\"ANTHROPIC_COMPAT_PROVIDERS\", \"DEEPSEEK,KIMI\");\n using var __ = EnvScope.Set(\"DEEPSEEK_ANTHROPIC_URL\", \"https://api.deepseek.com/anthropic\");\n using var ___ = EnvScope.Set(\"DEEPSEEK_APIKEY\", \"sk-deepseek\");\n using var ____ = EnvScope.Set(\"DEEPSEEK_MODELS\", \"deepseek-v4-pro\");\n using var _____ = EnvScope.Set(\"KIMI_ANTHROPIC_URL\", \"https://api.kimi.com/coding\");\n using var ______ = EnvScope.Set(\"KIMI_APIKEY\", \"sk-kimi\");\n using var _______ = EnvScope.Set(\"KIMI_MODELS\", \"kimi-2.5\");\n\n var models = AnthropicCompatProviders.DiscoverFromEnv(NullLoggerFactory.Instance);\n\n models.Select(m => m.Id).Should().BeEquivalentTo([\"deepseek-v4-pro\", \"kimi-2-5\"]);\n models.Single(m => m.FamilyKey == \"DEEPSEEK\").BaseUrl.Should().Be(\"https://api.deepseek.com/anthropic\");\n models.Single(m => m.FamilyKey == \"KIMI\").BaseUrl.Should().Be(\"https://api.kimi.com/coding\");\n }\n\n [Theory]\n [InlineData(null, \"sk-key\", \"model-a\")]\n [InlineData(\"https://example.com/anthropic\", null, \"model-a\")]\n [InlineData(\"https://example.com/anthropic\", \"sk-key\", null)]\n public void DiscoverFromEnv_SkipsFamily_WhenAnyOfItsThreeVarsIsMissing(\n string? baseUrl,\n string? apiKey,\n string? modelsRaw)\n {\n using var _ = EnvScope.Set(\"ANTHROPIC_COMPAT_PROVIDERS\", \"ACME\");\n using var __ = EnvScope.Set(\"ACME_ANTHROPIC_URL\", baseUrl);\n using var ___ = EnvScope.Set(\"ACME_APIKEY\", apiKey);\n using var ____ = EnvScope.Set(\"ACME_MODELS\", modelsRaw);\n\n var models = AnthropicCompatProviders.DiscoverFromEnv(NullLoggerFactory.Instance);\n\n models.Should().BeEmpty();\n }\n\n [Fact]\n public void DiscoverFromEnv_SkipsOnlyTheIncompleteFamily_KeepsOthers()\n {\n using var _ = EnvScope.Set(\"ANTHROPIC_COMPAT_PROVIDERS\", \"DEEPSEEK,BROKEN\");\n using var __ = EnvScope.Set(\"DEEPSEEK_ANTHROPIC_URL\", \"https://api.deepseek.com/anthropic\");\n using var ___ = EnvScope.Set(\"DEEPSEEK_APIKEY\", \"sk-deepseek\");\n using var ____ = EnvScope.Set(\"DEEPSEEK_MODELS\", \"deepseek-v4-pro\");\n using var _____ = EnvScope.Set(\"BROKEN_ANTHROPIC_URL\", null);\n using var ______ = EnvScope.Set(\"BROKEN_APIKEY\", \"sk-broken\");\n using var _______ = EnvScope.Set(\"BROKEN_MODELS\", \"broken-model\");\n\n var models = AnthropicCompatProviders.DiscoverFromEnv(NullLoggerFactory.Instance);\n\n models.Select(m => m.Id).Should().BeEquivalentTo([\"deepseek-v4-pro\"]);\n }\n\n [Fact]\n public void DiscoverFromEnv_SlugifiesNonAlphanumericCharacters_ForId_ButKeepsModelNameVerbatim()\n {\n using var _ = EnvScope.Set(\"ANTHROPIC_COMPAT_PROVIDERS\", \"DEEPSEEK\");\n using var __ = EnvScope.Set(\"DEEPSEEK_ANTHROPIC_URL\", \"https://api.deepseek.com/anthropic\");\n using var ___ = EnvScope.Set(\"DEEPSEEK_APIKEY\", \"sk-deepseek\");\n using var ____ = EnvScope.Set(\"DEEPSEEK_MODELS\", \"DeepSeek V4.Pro!\");\n\n var models = AnthropicCompatProviders.DiscoverFromEnv(NullLoggerFactory.Instance);\n\n var model = models.Single();\n model.Id.Should().Be(\"deepseek-v4-pro-\");\n model.ModelName.Should().Be(\"DeepSeek V4.Pro!\");\n model.DisplayName.Should().Be(\"DeepSeek V4.Pro!\");\n }\n\n [Fact]\n public void DiscoverFromEnv_TrimsAndIgnoresEmptyEntries_InModelsList()\n {\n using var _ = EnvScope.Set(\"ANTHROPIC_COMPAT_PROVIDERS\", \" DEEPSEEK , \");\n using var __ = EnvScope.Set(\"DEEPSEEK_ANTHROPIC_URL\", \"https://api.deepseek.com/anthropic\");\n using var ___ = EnvScope.Set(\"DEEPSEEK_APIKEY\", \"sk-deepseek\");\n using var ____ = EnvScope.Set(\"DEEPSEEK_MODELS\", \" deepseek-v4-pro ,, deepseek-v4-flash \");\n\n var models = AnthropicCompatProviders.DiscoverFromEnv(NullLoggerFactory.Instance);\n\n models.Select(m => m.Id).Should().BeEquivalentTo([\"deepseek-v4-pro\", \"deepseek-v4-flash\"]);\n }\n\n [Fact]\n public void DiscoverFromEnv_DropsSameFamilySlugCollision_KeepingFirstAndWarning()\n {\n // \"kimi-2.5\" and \"kimi-2-5\" both slugify to \"kimi-2-5\" — a same-family collision that would\n // otherwise let ProviderRegistry silently drop the second entry.\n using var _ = EnvScope.Set(\"ANTHROPIC_COMPAT_PROVIDERS\", \"KIMI\");\n using var __ = EnvScope.Set(\"KIMI_ANTHROPIC_URL\", \"https://api.kimi.com/coding\");\n using var ___ = EnvScope.Set(\"KIMI_APIKEY\", \"sk-kimi\");\n using var ____ = EnvScope.Set(\"KIMI_MODELS\", \"kimi-2.5,kimi-2-5\");\n\n var factory = new CapturingLoggerFactory();\n var models = AnthropicCompatProviders.DiscoverFromEnv(factory);\n\n // First configured entry wins; the colliding second is dropped, not silently overwritten.\n models.Select(m => m.ModelName).Should().BeEquivalentTo([\"kimi-2.5\"]);\n models.Single().Id.Should().Be(\"kimi-2-5\");\n factory.Entries\n .Should()\n .ContainSingle(e => e.Level == LogLevel.Warning && e.Message.Contains(\"collides\") && e.Message.Contains(\"kimi-2-5\"));\n }\n\n [Fact]\n public void DiscoverFromEnv_DropsCrossFamilyIdCollision_KeepingFirstAndWarning()\n {\n // The same model name configured under two families generates the same id — a cross-family\n // collision. Keep the first family's entry and warn rather than silently routing the shared id\n // through the wrong family.\n using var _ = EnvScope.Set(\"ANTHROPIC_COMPAT_PROVIDERS\", \"DEEPSEEK,KIMI\");\n using var __ = EnvScope.Set(\"DEEPSEEK_ANTHROPIC_URL\", \"https://api.deepseek.com/anthropic\");\n using var ___ = EnvScope.Set(\"DEEPSEEK_APIKEY\", \"sk-deepseek\");\n using var ____ = EnvScope.Set(\"DEEPSEEK_MODELS\", \"shared-model\");\n using var _____ = EnvScope.Set(\"KIMI_ANTHROPIC_URL\", \"https://api.kimi.com/coding\");\n using var ______ = EnvScope.Set(\"KIMI_APIKEY\", \"sk-kimi\");\n using var _______ = EnvScope.Set(\"KIMI_MODELS\", \"shared-model\");\n\n var factory = new CapturingLoggerFactory();\n var models = AnthropicCompatProviders.DiscoverFromEnv(factory);\n\n var model = models.Should().ContainSingle().Subject;\n model.Id.Should().Be(\"shared-model\");\n model.FamilyKey.Should().Be(\"DEEPSEEK\");\n factory.Entries\n .Should()\n .ContainSingle(e => e.Level == LogLevel.Warning && e.Message.Contains(\"collides\") && e.Message.Contains(\"KIMI\"));\n }\n\n /// <summary>\n /// Minimal <see cref=\"ILoggerFactory\"/> capturing every entry across all created (named) loggers,\n /// so a test can assert the discovery path logs a clear collision warning. <c>DiscoverFromEnv</c>\n /// resolves a non-generic named logger, which the shared <c>CapturingLogger&lt;T&gt;</c> test double\n /// (an <c>ILogger&lt;T&gt;</c>) does not cover, so this small factory bridges the gap.\n /// </summary>\n private sealed class CapturingLoggerFactory : ILoggerFactory\n {\n private readonly List<(LogLevel Level, string Message)> _entries = [];\n\n public IReadOnlyList<(LogLevel Level, string Message)> Entries\n {\n get\n {\n lock (_entries)\n {\n return [.. _entries];\n }\n }\n }\n\n public ILogger CreateLogger(string categoryName) => new CapturingLogger(_entries);\n\n public void AddProvider(ILoggerProvider provider) { }\n\n public void Dispose() { }\n\n private sealed class CapturingLogger(List<(LogLevel Level, string Message)> entries) : ILogger\n {\n public IDisposable BeginScope<TState>(TState state)\n where TState : notnull => NullScope.Instance;\n\n public bool IsEnabled(LogLevel logLevel) => true;\n\n public void Log<TState>(\n LogLevel logLevel,\n EventId eventId,\n TState state,\n Exception? exception,\n Func<TState, Exception?, string> formatter)\n {\n var message = formatter(state, exception);\n lock (entries)\n {\n entries.Add((logLevel, message));\n }\n }\n\n private sealed class NullScope : IDisposable\n {\n public static readonly NullScope Instance = new();\n\n public void Dispose() { }\n }\n }\n }\n\n /// <summary>\n /// Snapshots the current value of an environment variable and restores it on dispose.\n /// Tests that mutate env vars must use this scope to avoid polluting other tests.\n /// </summary>\n private sealed class EnvScope : IDisposable\n {\n private readonly string _name;\n private readonly string? _previous;\n\n private EnvScope(string name, string? previous)\n {\n _name = name;\n _previous = previous;\n }\n\n public static EnvScope Set(string name, string? value)\n {\n var previous = Environment.GetEnvironmentVariable(name);\n Environment.SetEnvironmentVariable(name, value);\n return new EnvScope(name, previous);\n }\n\n public void Dispose()\n {\n Environment.SetEnvironmentVariable(_name, _previous);\n }\n }\n}\n"}, "5c7fc75b3dc6563a2a4c5b81f277d020ab247b9a": {"t": 1785756728.6091518, "n": 11524, "h": "4b39d91b2ec611a4", "c": "using LmStreaming.Sample.Services;\nusing Microsoft.Extensions.Logging;\n\nnamespace LmStreaming.Sample.Tests.Services;\n\n/// <summary>\n/// Tests for <see cref=\"AnthropicCompatProviders.DiscoverFromEnv\" /> — the env-var-driven discovery\n/// of generic Anthropic-compatible provider families (e.g. DeepSeek). Mirrors the \"degrade to empty\n/// list, never throw\" contract of the Copilot model discovery path.\n/// </summary>\n[Collection(\"EnvironmentVariables\")]\npublic class AnthropicCompatProvidersTests\n{\n [Fact]\n public void DiscoverFromEnv_ReturnsEmpty_WhenProvidersListUnset()\n {\n using var _ = EnvScope.Set(\"ANTHROPIC_COMPAT_PROVIDERS\", null);\n\n var models = AnthropicCompatProviders.DiscoverFromEnv(NullLoggerFactory.Instance);\n\n models.Should().BeEmpty();\n }\n\n [Fact]\n public void DiscoverFromEnv_ReturnsEmpty_WhenProvidersListBlank()\n {\n using var _ = EnvScope.Set(\"ANTHROPIC_COMPAT_PROVIDERS\", \" \");\n\n var models = AnthropicCompatProviders.DiscoverFromEnv(NullLoggerFactory.Instance);\n\n models.Should().BeEmpty();\n }\n\n [Fact]\n public void DiscoverFromEnv_ParsesSingleFamily_WithMultipleModels()\n {\n using var _ = EnvScope.Set(\"ANTHROPIC_COMPAT_PROVIDERS\", \"DEEPSEEK\");\n using var __ = EnvScope.Set(\"DEEPSEEK_ANTHROPIC_URL\", \"https://api.deepseek.com/anthropic\");\n using var ___ = EnvScope.Set(\"DEEPSEEK_APIKEY\", \"sk-deepseek\");\n using var ____ = EnvScope.Set(\"DEEPSEEK_MODELS\", \"deepseek-v4-pro,deepseek-v4-flash\");\n\n var models = AnthropicCompatProviders.DiscoverFromEnv(NullLoggerFactory.Instance);\n\n models.Should().BeEquivalentTo(\n [\n new AnthropicCompatModel(\n \"deepseek-v4-pro\", \"deepseek-v4-pro\", \"deepseek-v4-pro\",\n \"https://api.deepseek.com/anthropic\", \"sk-deepseek\", \"DEEPSEEK\"),\n new AnthropicCompatModel(\n \"deepseek-v4-flash\", \"deepseek-v4-flash\", \"deepseek-v4-flash\",\n \"https://api.deepseek.com/anthropic\", \"sk-deepseek\", \"DEEPSEEK\"),\n ],\n options => options.WithStrictOrdering());\n }\n\n [Fact]\n public void DiscoverFromEnv_ParsesMultipleFamilies()\n {\n using var _ = EnvScope.Set(\"ANTHROPIC_COMPAT_PROVIDERS\", \"DEEPSEEK,KIMI\");\n using var __ = EnvScope.Set(\"DEEPSEEK_ANTHROPIC_URL\", \"https://api.deepseek.com/anthropic\");\n using var ___ = EnvScope.Set(\"DEEPSEEK_APIKEY\", \"sk-deepseek\");\n using var ____ = EnvScope.Set(\"DEEPSEEK_MODELS\", \"deepseek-v4-pro\");\n using var _____ = EnvScope.Set(\"KIMI_ANTHROPIC_URL\", \"https://api.kimi.com/coding\");\n using var ______ = EnvScope.Set(\"KIMI_APIKEY\", \"sk-kimi\");\n using var _______ = EnvScope.Set(\"KIMI_MODELS\", \"kimi-2.5\");\n\n var models = AnthropicCompatProviders.DiscoverFromEnv(NullLoggerFactory.Instance);\n\n models.Select(m => m.Id).Should().BeEquivalentTo([\"deepseek-v4-pro\", \"kimi-2-5\"]);\n models.Single(m => m.FamilyKey == \"DEEPSEEK\").BaseUrl.Should().Be(\"https://api.deepseek.com/anthropic\");\n models.Single(m => m.FamilyKey == \"KIMI\").BaseUrl.Should().Be(\"https://api.kimi.com/coding\");\n }\n\n [Theory]\n [InlineData(null, \"sk-key\", \"model-a\")]\n [InlineData(\"https://example.com/anthropic\", null, \"model-a\")]\n [InlineData(\"https://example.com/anthropic\", \"sk-key\", null)]\n public void DiscoverFromEnv_SkipsFamily_WhenAnyOfItsThreeVarsIsMissing(\n string? baseUrl,\n string? apiKey,\n string? modelsRaw)\n {\n using var _ = EnvScope.Set(\"ANTHROPIC_COMPAT_PROVIDERS\", \"ACME\");\n using var __ = EnvScope.Set(\"ACME_ANTHROPIC_URL\", baseUrl);\n using var ___ = EnvScope.Set(\"ACME_APIKEY\", apiKey);\n using var ____ = EnvScope.Set(\"ACME_MODELS\", modelsRaw);\n\n var models = AnthropicCompatProviders.DiscoverFromEnv(NullLoggerFactory.Instance);\n\n models.Should().BeEmpty();\n }\n\n [Fact]\n public void DiscoverFromEnv_SkipsOnlyTheIncompleteFamily_KeepsOthers()\n {\n using var _ = EnvScope.Set(\"ANTHROPIC_COMPAT_PROVIDERS\", \"DEEPSEEK,BROKEN\");\n using var __ = EnvScope.Set(\"DEEPSEEK_ANTHROPIC_URL\", \"https://api.deepseek.com/anthropic\");\n using var ___ = EnvScope.Set(\"DEEPSEEK_APIKEY\", \"sk-deepseek\");\n using var ____ = EnvScope.Set(\"DEEPSEEK_MODELS\", \"deepseek-v4-pro\");\n using var _____ = EnvScope.Set(\"BROKEN_ANTHROPIC_URL\", null);\n using var ______ = EnvScope.Set(\"BROKEN_APIKEY\", \"sk-broken\");\n using var _______ = EnvScope.Set(\"BROKEN_MODELS\", \"broken-model\");\n\n var models = AnthropicCompatProviders.DiscoverFromEnv(NullLoggerFactory.Instance);\n\n models.Select(m => m.Id).Should().BeEquivalentTo([\"deepseek-v4-pro\"]);\n }\n\n [Fact]\n public void DiscoverFromEnv_SlugifiesNonAlphanumericCharacters_ForId_ButKeepsModelNameVerbatim()\n {\n using var _ = EnvScope.Set(\"ANTHROPIC_COMPAT_PROVIDERS\", \"DEEPSEEK\");\n using var __ = EnvScope.Set(\"DEEPSEEK_ANTHROPIC_URL\", \"https://api.deepseek.com/anthropic\");\n using var ___ = EnvScope.Set(\"DEEPSEEK_APIKEY\", \"sk-deepseek\");\n using var ____ = EnvScope.Set(\"DEEPSEEK_MODELS\", \"DeepSeek V4.Pro!\");\n\n var models = AnthropicCompatProviders.DiscoverFromEnv(NullLoggerFactory.Instance);\n\n var model = models.Single();\n model.Id.Should().Be(\"deepseek-v4-pro-\");\n model.ModelName.Should().Be(\"DeepSeek V4.Pro!\");\n model.DisplayName.Should().Be(\"DeepSeek V4.Pro!\");\n }\n\n [Fact]\n public void DiscoverFromEnv_TrimsAndIgnoresEmptyEntries_InModelsList()\n {\n using var _ = EnvScope.Set(\"ANTHROPIC_COMPAT_PROVIDERS\", \" DEEPSEEK , \");\n using var __ = EnvScope.Set(\"DEEPSEEK_ANTHROPIC_URL\", \"https://api.deepseek.com/anthropic\");\n using var ___ = EnvScope.Set(\"DEEPSEEK_APIKEY\", \"sk-deepseek\");\n using var ____ = EnvScope.Set(\"DEEPSEEK_MODELS\", \" deepseek-v4-pro ,, deepseek-v4-flash \");\n\n var models = AnthropicCompatProviders.DiscoverFromEnv(NullLoggerFactory.Instance);\n\n models.Select(m => m.Id).Should().BeEquivalentTo([\"deepseek-v4-pro\", \"deepseek-v4-flash\"]);\n }\n\n [Fact]\n public void DiscoverFromEnv_DropsSameFamilySlugCollision_KeepingFirstAndWarning()\n {\n // \"kimi-2.5\" and \"kimi-2-5\" both slugify to \"kimi-2-5\" — a same-family collision that would\n // otherwise let ProviderRegistry silently drop the second entry.\n using var _ = EnvScope.Set(\"ANTHROPIC_COMPAT_PROVIDERS\", \"KIMI\");\n using var __ = EnvScope.Set(\"KIMI_ANTHROPIC_URL\", \"https://api.kimi.com/coding\");\n using var ___ = EnvScope.Set(\"KIMI_APIKEY\", \"sk-kimi\");\n using var ____ = EnvScope.Set(\"KIMI_MODELS\", \"kimi-2.5,kimi-2-5\");\n\n var factory = new CapturingLoggerFactory();\n var models = AnthropicCompatProviders.DiscoverFromEnv(factory);\n\n // First configured entry wins; the colliding second is dropped, not silently overwritten.\n models.Select(m => m.ModelName).Should().BeEquivalentTo([\"kimi-2.5\"]);\n models.Single().Id.Should().Be(\"kimi-2-5\");\n factory.Entries\n .Should()\n .ContainSingle(e => e.Level == LogLevel.Warning && e.Message.Contains(\"collides\") && e.Message.Contains(\"kimi-2-5\"));\n }\n\n [Fact]\n public void DiscoverFromEnv_DropsCrossFamilyIdCollision_KeepingFirstAndWarning()\n {\n // The same model name configured under two families generates the same id — a cross-family\n // collision. Keep the first family's entry and warn rather than silently routing the shared id\n // through the wrong family.\n using var _ = EnvScope.Set(\"ANTHROPIC_COMPAT_PROVIDERS\", \"DEEPSEEK,KIMI\");\n using var __ = EnvScope.Set(\"DEEPSEEK_ANTHROPIC_URL\", \"https://api.deepseek.com/anthropic\");\n using var ___ = EnvScope.Set(\"DEEPSEEK_APIKEY\", \"sk-deepseek\");\n using var ____ = EnvScope.Set(\"DEEPSEEK_MODELS\", \"shared-model\");\n using var _____ = EnvScope.Set(\"KIMI_ANTHROPIC_URL\", \"https://api.kimi.com/coding\");\n using var ______ = EnvScope.Set(\"KIMI_APIKEY\", \"sk-kimi\");\n using var _______ = EnvScope.Set(\"KIMI_MODELS\", \"shared-model\");\n\n var factory = new CapturingLoggerFactory();\n var models = AnthropicCompatProviders.DiscoverFromEnv(factory);\n\n var model = models.Should().ContainSingle().Subject;\n model.Id.Should().Be(\"shared-model\");\n model.FamilyKey.Should().Be(\"DEEPSEEK\");\n factory.Entries\n .Should()\n .ContainSingle(e => e.Level == LogLevel.Warning && e.Message.Contains(\"collides\") && e.Message.Contains(\"KIMI\"));\n }\n\n /// <summary>\n /// Minimal <see cref=\"ILoggerFactory\"/> capturing every entry across all created (named) loggers,\n /// so a test can assert the discovery path logs a clear collision warning. <c>DiscoverFromEnv</c>\n /// resolves a non-generic named logger, which the shared <c>CapturingLogger&lt;T&gt;</c> test double\n /// (an <c>ILogger&lt;T&gt;</c>) does not cover, so this small factory bridges the gap.\n /// </summary>\n private sealed class CapturingLoggerFactory : ILoggerFactory\n {\n private readonly List<(LogLevel Level, string Message)> _entries = [];\n\n public IReadOnlyList<(LogLevel Level, string Message)> Entries\n {\n get\n {\n lock (_entries)\n {\n return [.. _entries];\n }\n }\n }\n\n public ILogger CreateLogger(string categoryName) => new CapturingLogger(_entries);\n\n public void AddProvider(ILoggerProvider provider) { }\n\n public void Dispose() { }\n\n private sealed class CapturingLogger(List<(LogLevel Level, string Message)> entries) : ILogger\n {\n public IDisposable BeginScope<TState>(TState state)\n where TState : notnull => NullScope.Instance;\n\n public bool IsEnabled(LogLevel logLevel) => true;\n\n public void Log<TState>(\n LogLevel logLevel,\n EventId eventId,\n TState state,\n Exception? exception,\n Func<TState, Exception?, string> formatter)\n {\n var message = formatter(state, exception);\n lock (entries)\n {\n entries.Add((logLevel, message));\n }\n }\n\n private sealed class NullScope : IDisposable\n {\n public static readonly NullScope Instance = new();\n\n public void Dispose() { }\n }\n }\n }\n\n /// <summary>\n /// Snapshots the current value of an environment variable and restores it on dispose.\n /// Tests that mutate env vars must use this scope to avoid polluting other tests.\n /// </summary>\n private sealed class EnvScope : IDisposable\n {\n private readonly string _name;\n private readonly string? _previous;\n\n private EnvScope(string name, string? previous)\n {\n _name = name;\n _previous = previous;\n }\n\n public static EnvScope Set(string name, string? value)\n {\n var previous = Environment.GetEnvironmentVariable(name);\n Environment.SetEnvironmentVariable(name, value);\n return new EnvScope(name, previous);\n }\n\n public void Dispose()\n {\n Environment.SetEnvironmentVariable(_name, _previous);\n }\n }\n}\n"}, "ddd4d029a734677e4b28624bd4663e1143d96e86": {"t": 1786067627.5893624, "n": 11524, "h": "4b39d91b2ec611a4", "c": "using LmStreaming.Sample.Services;\nusing Microsoft.Extensions.Logging;\n\nnamespace LmStreaming.Sample.Tests.Services;\n\n/// <summary>\n/// Tests for <see cref=\"AnthropicCompatProviders.DiscoverFromEnv\" /> — the env-var-driven discovery\n/// of generic Anthropic-compatible provider families (e.g. DeepSeek). Mirrors the \"degrade to empty\n/// list, never throw\" contract of the Copilot model discovery path.\n/// </summary>\n[Collection(\"EnvironmentVariables\")]\npublic class AnthropicCompatProvidersTests\n{\n [Fact]\n public void DiscoverFromEnv_ReturnsEmpty_WhenProvidersListUnset()\n {\n using var _ = EnvScope.Set(\"ANTHROPIC_COMPAT_PROVIDERS\", null);\n\n var models = AnthropicCompatProviders.DiscoverFromEnv(NullLoggerFactory.Instance);\n\n models.Should().BeEmpty();\n }\n\n [Fact]\n public void DiscoverFromEnv_ReturnsEmpty_WhenProvidersListBlank()\n {\n using var _ = EnvScope.Set(\"ANTHROPIC_COMPAT_PROVIDERS\", \" \");\n\n var models = AnthropicCompatProviders.DiscoverFromEnv(NullLoggerFactory.Instance);\n\n models.Should().BeEmpty();\n }\n\n [Fact]\n public void DiscoverFromEnv_ParsesSingleFamily_WithMultipleModels()\n {\n using var _ = EnvScope.Set(\"ANTHROPIC_COMPAT_PROVIDERS\", \"DEEPSEEK\");\n using var __ = EnvScope.Set(\"DEEPSEEK_ANTHROPIC_URL\", \"https://api.deepseek.com/anthropic\");\n using var ___ = EnvScope.Set(\"DEEPSEEK_APIKEY\", \"sk-deepseek\");\n using var ____ = EnvScope.Set(\"DEEPSEEK_MODELS\", \"deepseek-v4-pro,deepseek-v4-flash\");\n\n var models = AnthropicCompatProviders.DiscoverFromEnv(NullLoggerFactory.Instance);\n\n models.Should().BeEquivalentTo(\n [\n new AnthropicCompatModel(\n \"deepseek-v4-pro\", \"deepseek-v4-pro\", \"deepseek-v4-pro\",\n \"https://api.deepseek.com/anthropic\", \"sk-deepseek\", \"DEEPSEEK\"),\n new AnthropicCompatModel(\n \"deepseek-v4-flash\", \"deepseek-v4-flash\", \"deepseek-v4-flash\",\n \"https://api.deepseek.com/anthropic\", \"sk-deepseek\", \"DEEPSEEK\"),\n ],\n options => options.WithStrictOrdering());\n }\n\n [Fact]\n public void DiscoverFromEnv_ParsesMultipleFamilies()\n {\n using var _ = EnvScope.Set(\"ANTHROPIC_COMPAT_PROVIDERS\", \"DEEPSEEK,KIMI\");\n using var __ = EnvScope.Set(\"DEEPSEEK_ANTHROPIC_URL\", \"https://api.deepseek.com/anthropic\");\n using var ___ = EnvScope.Set(\"DEEPSEEK_APIKEY\", \"sk-deepseek\");\n using var ____ = EnvScope.Set(\"DEEPSEEK_MODELS\", \"deepseek-v4-pro\");\n using var _____ = EnvScope.Set(\"KIMI_ANTHROPIC_URL\", \"https://api.kimi.com/coding\");\n using var ______ = EnvScope.Set(\"KIMI_APIKEY\", \"sk-kimi\");\n using var _______ = EnvScope.Set(\"KIMI_MODELS\", \"kimi-2.5\");\n\n var models = AnthropicCompatProviders.DiscoverFromEnv(NullLoggerFactory.Instance);\n\n models.Select(m => m.Id).Should().BeEquivalentTo([\"deepseek-v4-pro\", \"kimi-2-5\"]);\n models.Single(m => m.FamilyKey == \"DEEPSEEK\").BaseUrl.Should().Be(\"https://api.deepseek.com/anthropic\");\n models.Single(m => m.FamilyKey == \"KIMI\").BaseUrl.Should().Be(\"https://api.kimi.com/coding\");\n }\n\n [Theory]\n [InlineData(null, \"sk-key\", \"model-a\")]\n [InlineData(\"https://example.com/anthropic\", null, \"model-a\")]\n [InlineData(\"https://example.com/anthropic\", \"sk-key\", null)]\n public void DiscoverFromEnv_SkipsFamily_WhenAnyOfItsThreeVarsIsMissing(\n string? baseUrl,\n string? apiKey,\n string? modelsRaw)\n {\n using var _ = EnvScope.Set(\"ANTHROPIC_COMPAT_PROVIDERS\", \"ACME\");\n using var __ = EnvScope.Set(\"ACME_ANTHROPIC_URL\", baseUrl);\n using var ___ = EnvScope.Set(\"ACME_APIKEY\", apiKey);\n using var ____ = EnvScope.Set(\"ACME_MODELS\", modelsRaw);\n\n var models = AnthropicCompatProviders.DiscoverFromEnv(NullLoggerFactory.Instance);\n\n models.Should().BeEmpty();\n }\n\n [Fact]\n public void DiscoverFromEnv_SkipsOnlyTheIncompleteFamily_KeepsOthers()\n {\n using var _ = EnvScope.Set(\"ANTHROPIC_COMPAT_PROVIDERS\", \"DEEPSEEK,BROKEN\");\n using var __ = EnvScope.Set(\"DEEPSEEK_ANTHROPIC_URL\", \"https://api.deepseek.com/anthropic\");\n using var ___ = EnvScope.Set(\"DEEPSEEK_APIKEY\", \"sk-deepseek\");\n using var ____ = EnvScope.Set(\"DEEPSEEK_MODELS\", \"deepseek-v4-pro\");\n using var _____ = EnvScope.Set(\"BROKEN_ANTHROPIC_URL\", null);\n using var ______ = EnvScope.Set(\"BROKEN_APIKEY\", \"sk-broken\");\n using var _______ = EnvScope.Set(\"BROKEN_MODELS\", \"broken-model\");\n\n var models = AnthropicCompatProviders.DiscoverFromEnv(NullLoggerFactory.Instance);\n\n models.Select(m => m.Id).Should().BeEquivalentTo([\"deepseek-v4-pro\"]);\n }\n\n [Fact]\n public void DiscoverFromEnv_SlugifiesNonAlphanumericCharacters_ForId_ButKeepsModelNameVerbatim()\n {\n using var _ = EnvScope.Set(\"ANTHROPIC_COMPAT_PROVIDERS\", \"DEEPSEEK\");\n using var __ = EnvScope.Set(\"DEEPSEEK_ANTHROPIC_URL\", \"https://api.deepseek.com/anthropic\");\n using var ___ = EnvScope.Set(\"DEEPSEEK_APIKEY\", \"sk-deepseek\");\n using var ____ = EnvScope.Set(\"DEEPSEEK_MODELS\", \"DeepSeek V4.Pro!\");\n\n var models = AnthropicCompatProviders.DiscoverFromEnv(NullLoggerFactory.Instance);\n\n var model = models.Single();\n model.Id.Should().Be(\"deepseek-v4-pro-\");\n model.ModelName.Should().Be(\"DeepSeek V4.Pro!\");\n model.DisplayName.Should().Be(\"DeepSeek V4.Pro!\");\n }\n\n [Fact]\n public void DiscoverFromEnv_TrimsAndIgnoresEmptyEntries_InModelsList()\n {\n using var _ = EnvScope.Set(\"ANTHROPIC_COMPAT_PROVIDERS\", \" DEEPSEEK , \");\n using var __ = EnvScope.Set(\"DEEPSEEK_ANTHROPIC_URL\", \"https://api.deepseek.com/anthropic\");\n using var ___ = EnvScope.Set(\"DEEPSEEK_APIKEY\", \"sk-deepseek\");\n using var ____ = EnvScope.Set(\"DEEPSEEK_MODELS\", \" deepseek-v4-pro ,, deepseek-v4-flash \");\n\n var models = AnthropicCompatProviders.DiscoverFromEnv(NullLoggerFactory.Instance);\n\n models.Select(m => m.Id).Should().BeEquivalentTo([\"deepseek-v4-pro\", \"deepseek-v4-flash\"]);\n }\n\n [Fact]\n public void DiscoverFromEnv_DropsSameFamilySlugCollision_KeepingFirstAndWarning()\n {\n // \"kimi-2.5\" and \"kimi-2-5\" both slugify to \"kimi-2-5\" — a same-family collision that would\n // otherwise let ProviderRegistry silently drop the second entry.\n using var _ = EnvScope.Set(\"ANTHROPIC_COMPAT_PROVIDERS\", \"KIMI\");\n using var __ = EnvScope.Set(\"KIMI_ANTHROPIC_URL\", \"https://api.kimi.com/coding\");\n using var ___ = EnvScope.Set(\"KIMI_APIKEY\", \"sk-kimi\");\n using var ____ = EnvScope.Set(\"KIMI_MODELS\", \"kimi-2.5,kimi-2-5\");\n\n var factory = new CapturingLoggerFactory();\n var models = AnthropicCompatProviders.DiscoverFromEnv(factory);\n\n // First configured entry wins; the colliding second is dropped, not silently overwritten.\n models.Select(m => m.ModelName).Should().BeEquivalentTo([\"kimi-2.5\"]);\n models.Single().Id.Should().Be(\"kimi-2-5\");\n factory.Entries\n .Should()\n .ContainSingle(e => e.Level == LogLevel.Warning && e.Message.Contains(\"collides\") && e.Message.Contains(\"kimi-2-5\"));\n }\n\n [Fact]\n public void DiscoverFromEnv_DropsCrossFamilyIdCollision_KeepingFirstAndWarning()\n {\n // The same model name configured under two families generates the same id — a cross-family\n // collision. Keep the first family's entry and warn rather than silently routing the shared id\n // through the wrong family.\n using var _ = EnvScope.Set(\"ANTHROPIC_COMPAT_PROVIDERS\", \"DEEPSEEK,KIMI\");\n using var __ = EnvScope.Set(\"DEEPSEEK_ANTHROPIC_URL\", \"https://api.deepseek.com/anthropic\");\n using var ___ = EnvScope.Set(\"DEEPSEEK_APIKEY\", \"sk-deepseek\");\n using var ____ = EnvScope.Set(\"DEEPSEEK_MODELS\", \"shared-model\");\n using var _____ = EnvScope.Set(\"KIMI_ANTHROPIC_URL\", \"https://api.kimi.com/coding\");\n using var ______ = EnvScope.Set(\"KIMI_APIKEY\", \"sk-kimi\");\n using var _______ = EnvScope.Set(\"KIMI_MODELS\", \"shared-model\");\n\n var factory = new CapturingLoggerFactory();\n var models = AnthropicCompatProviders.DiscoverFromEnv(factory);\n\n var model = models.Should().ContainSingle().Subject;\n model.Id.Should().Be(\"shared-model\");\n model.FamilyKey.Should().Be(\"DEEPSEEK\");\n factory.Entries\n .Should()\n .ContainSingle(e => e.Level == LogLevel.Warning && e.Message.Contains(\"collides\") && e.Message.Contains(\"KIMI\"));\n }\n\n /// <summary>\n /// Minimal <see cref=\"ILoggerFactory\"/> capturing every entry across all created (named) loggers,\n /// so a test can assert the discovery path logs a clear collision warning. <c>DiscoverFromEnv</c>\n /// resolves a non-generic named logger, which the shared <c>CapturingLogger&lt;T&gt;</c> test double\n /// (an <c>ILogger&lt;T&gt;</c>) does not cover, so this small factory bridges the gap.\n /// </summary>\n private sealed class CapturingLoggerFactory : ILoggerFactory\n {\n private readonly List<(LogLevel Level, string Message)> _entries = [];\n\n public IReadOnlyList<(LogLevel Level, string Message)> Entries\n {\n get\n {\n lock (_entries)\n {\n return [.. _entries];\n }\n }\n }\n\n public ILogger CreateLogger(string categoryName) => new CapturingLogger(_entries);\n\n public void AddProvider(ILoggerProvider provider) { }\n\n public void Dispose() { }\n\n private sealed class CapturingLogger(List<(LogLevel Level, string Message)> entries) : ILogger\n {\n public IDisposable BeginScope<TState>(TState state)\n where TState : notnull => NullScope.Instance;\n\n public bool IsEnabled(LogLevel logLevel) => true;\n\n public void Log<TState>(\n LogLevel logLevel,\n EventId eventId,\n TState state,\n Exception? exception,\n Func<TState, Exception?, string> formatter)\n {\n var message = formatter(state, exception);\n lock (entries)\n {\n entries.Add((logLevel, message));\n }\n }\n\n private sealed class NullScope : IDisposable\n {\n public static readonly NullScope Instance = new();\n\n public void Dispose() { }\n }\n }\n }\n\n /// <summary>\n /// Snapshots the current value of an environment variable and restores it on dispose.\n /// Tests that mutate env vars must use this scope to avoid polluting other tests.\n /// </summary>\n private sealed class EnvScope : IDisposable\n {\n private readonly string _name;\n private readonly string? _previous;\n\n private EnvScope(string name, string? previous)\n {\n _name = name;\n _previous = previous;\n }\n\n public static EnvScope Set(string name, string? value)\n {\n var previous = Environment.GetEnvironmentVariable(name);\n Environment.SetEnvironmentVariable(name, value);\n return new EnvScope(name, previous);\n }\n\n public void Dispose()\n {\n Environment.SetEnvironmentVariable(_name, _previous);\n }\n }\n}\n"}, "3a25297b94f73fcda85e6e48bc8b48c2ee8e662d": {"t": 1786068389.0838702, "n": 11524, "h": "4b39d91b2ec611a4", "c": "using LmStreaming.Sample.Services;\nusing Microsoft.Extensions.Logging;\n\nnamespace LmStreaming.Sample.Tests.Services;\n\n/// <summary>\n/// Tests for <see cref=\"AnthropicCompatProviders.DiscoverFromEnv\" /> — the env-var-driven discovery\n/// of generic Anthropic-compatible provider families (e.g. DeepSeek). Mirrors the \"degrade to empty\n/// list, never throw\" contract of the Copilot model discovery path.\n/// </summary>\n[Collection(\"EnvironmentVariables\")]\npublic class AnthropicCompatProvidersTests\n{\n [Fact]\n public void DiscoverFromEnv_ReturnsEmpty_WhenProvidersListUnset()\n {\n using var _ = EnvScope.Set(\"ANTHROPIC_COMPAT_PROVIDERS\", null);\n\n var models = AnthropicCompatProviders.DiscoverFromEnv(NullLoggerFactory.Instance);\n\n models.Should().BeEmpty();\n }\n\n [Fact]\n public void DiscoverFromEnv_ReturnsEmpty_WhenProvidersListBlank()\n {\n using var _ = EnvScope.Set(\"ANTHROPIC_COMPAT_PROVIDERS\", \" \");\n\n var models = AnthropicCompatProviders.DiscoverFromEnv(NullLoggerFactory.Instance);\n\n models.Should().BeEmpty();\n }\n\n [Fact]\n public void DiscoverFromEnv_ParsesSingleFamily_WithMultipleModels()\n {\n using var _ = EnvScope.Set(\"ANTHROPIC_COMPAT_PROVIDERS\", \"DEEPSEEK\");\n using var __ = EnvScope.Set(\"DEEPSEEK_ANTHROPIC_URL\", \"https://api.deepseek.com/anthropic\");\n using var ___ = EnvScope.Set(\"DEEPSEEK_APIKEY\", \"sk-deepseek\");\n using var ____ = EnvScope.Set(\"DEEPSEEK_MODELS\", \"deepseek-v4-pro,deepseek-v4-flash\");\n\n var models = AnthropicCompatProviders.DiscoverFromEnv(NullLoggerFactory.Instance);\n\n models.Should().BeEquivalentTo(\n [\n new AnthropicCompatModel(\n \"deepseek-v4-pro\", \"deepseek-v4-pro\", \"deepseek-v4-pro\",\n \"https://api.deepseek.com/anthropic\", \"sk-deepseek\", \"DEEPSEEK\"),\n new AnthropicCompatModel(\n \"deepseek-v4-flash\", \"deepseek-v4-flash\", \"deepseek-v4-flash\",\n \"https://api.deepseek.com/anthropic\", \"sk-deepseek\", \"DEEPSEEK\"),\n ],\n options => options.WithStrictOrdering());\n }\n\n [Fact]\n public void DiscoverFromEnv_ParsesMultipleFamilies()\n {\n using var _ = EnvScope.Set(\"ANTHROPIC_COMPAT_PROVIDERS\", \"DEEPSEEK,KIMI\");\n using var __ = EnvScope.Set(\"DEEPSEEK_ANTHROPIC_URL\", \"https://api.deepseek.com/anthropic\");\n using var ___ = EnvScope.Set(\"DEEPSEEK_APIKEY\", \"sk-deepseek\");\n using var ____ = EnvScope.Set(\"DEEPSEEK_MODELS\", \"deepseek-v4-pro\");\n using var _____ = EnvScope.Set(\"KIMI_ANTHROPIC_URL\", \"https://api.kimi.com/coding\");\n using var ______ = EnvScope.Set(\"KIMI_APIKEY\", \"sk-kimi\");\n using var _______ = EnvScope.Set(\"KIMI_MODELS\", \"kimi-2.5\");\n\n var models = AnthropicCompatProviders.DiscoverFromEnv(NullLoggerFactory.Instance);\n\n models.Select(m => m.Id).Should().BeEquivalentTo([\"deepseek-v4-pro\", \"kimi-2-5\"]);\n models.Single(m => m.FamilyKey == \"DEEPSEEK\").BaseUrl.Should().Be(\"https://api.deepseek.com/anthropic\");\n models.Single(m => m.FamilyKey == \"KIMI\").BaseUrl.Should().Be(\"https://api.kimi.com/coding\");\n }\n\n [Theory]\n [InlineData(null, \"sk-key\", \"model-a\")]\n [InlineData(\"https://example.com/anthropic\", null, \"model-a\")]\n [InlineData(\"https://example.com/anthropic\", \"sk-key\", null)]\n public void DiscoverFromEnv_SkipsFamily_WhenAnyOfItsThreeVarsIsMissing(\n string? baseUrl,\n string? apiKey,\n string? modelsRaw)\n {\n using var _ = EnvScope.Set(\"ANTHROPIC_COMPAT_PROVIDERS\", \"ACME\");\n using var __ = EnvScope.Set(\"ACME_ANTHROPIC_URL\", baseUrl);\n using var ___ = EnvScope.Set(\"ACME_APIKEY\", apiKey);\n using var ____ = EnvScope.Set(\"ACME_MODELS\", modelsRaw);\n\n var models = AnthropicCompatProviders.DiscoverFromEnv(NullLoggerFactory.Instance);\n\n models.Should().BeEmpty();\n }\n\n [Fact]\n public void DiscoverFromEnv_SkipsOnlyTheIncompleteFamily_KeepsOthers()\n {\n using var _ = EnvScope.Set(\"ANTHROPIC_COMPAT_PROVIDERS\", \"DEEPSEEK,BROKEN\");\n using var __ = EnvScope.Set(\"DEEPSEEK_ANTHROPIC_URL\", \"https://api.deepseek.com/anthropic\");\n using var ___ = EnvScope.Set(\"DEEPSEEK_APIKEY\", \"sk-deepseek\");\n using var ____ = EnvScope.Set(\"DEEPSEEK_MODELS\", \"deepseek-v4-pro\");\n using var _____ = EnvScope.Set(\"BROKEN_ANTHROPIC_URL\", null);\n using var ______ = EnvScope.Set(\"BROKEN_APIKEY\", \"sk-broken\");\n using var _______ = EnvScope.Set(\"BROKEN_MODELS\", \"broken-model\");\n\n var models = AnthropicCompatProviders.DiscoverFromEnv(NullLoggerFactory.Instance);\n\n models.Select(m => m.Id).Should().BeEquivalentTo([\"deepseek-v4-pro\"]);\n }\n\n [Fact]\n public void DiscoverFromEnv_SlugifiesNonAlphanumericCharacters_ForId_ButKeepsModelNameVerbatim()\n {\n using var _ = EnvScope.Set(\"ANTHROPIC_COMPAT_PROVIDERS\", \"DEEPSEEK\");\n using var __ = EnvScope.Set(\"DEEPSEEK_ANTHROPIC_URL\", \"https://api.deepseek.com/anthropic\");\n using var ___ = EnvScope.Set(\"DEEPSEEK_APIKEY\", \"sk-deepseek\");\n using var ____ = EnvScope.Set(\"DEEPSEEK_MODELS\", \"DeepSeek V4.Pro!\");\n\n var models = AnthropicCompatProviders.DiscoverFromEnv(NullLoggerFactory.Instance);\n\n var model = models.Single();\n model.Id.Should().Be(\"deepseek-v4-pro-\");\n model.ModelName.Should().Be(\"DeepSeek V4.Pro!\");\n model.DisplayName.Should().Be(\"DeepSeek V4.Pro!\");\n }\n\n [Fact]\n public void DiscoverFromEnv_TrimsAndIgnoresEmptyEntries_InModelsList()\n {\n using var _ = EnvScope.Set(\"ANTHROPIC_COMPAT_PROVIDERS\", \" DEEPSEEK , \");\n using var __ = EnvScope.Set(\"DEEPSEEK_ANTHROPIC_URL\", \"https://api.deepseek.com/anthropic\");\n using var ___ = EnvScope.Set(\"DEEPSEEK_APIKEY\", \"sk-deepseek\");\n using var ____ = EnvScope.Set(\"DEEPSEEK_MODELS\", \" deepseek-v4-pro ,, deepseek-v4-flash \");\n\n var models = AnthropicCompatProviders.DiscoverFromEnv(NullLoggerFactory.Instance);\n\n models.Select(m => m.Id).Should().BeEquivalentTo([\"deepseek-v4-pro\", \"deepseek-v4-flash\"]);\n }\n\n [Fact]\n public void DiscoverFromEnv_DropsSameFamilySlugCollision_KeepingFirstAndWarning()\n {\n // \"kimi-2.5\" and \"kimi-2-5\" both slugify to \"kimi-2-5\" — a same-family collision that would\n // otherwise let ProviderRegistry silently drop the second entry.\n using var _ = EnvScope.Set(\"ANTHROPIC_COMPAT_PROVIDERS\", \"KIMI\");\n using var __ = EnvScope.Set(\"KIMI_ANTHROPIC_URL\", \"https://api.kimi.com/coding\");\n using var ___ = EnvScope.Set(\"KIMI_APIKEY\", \"sk-kimi\");\n using var ____ = EnvScope.Set(\"KIMI_MODELS\", \"kimi-2.5,kimi-2-5\");\n\n var factory = new CapturingLoggerFactory();\n var models = AnthropicCompatProviders.DiscoverFromEnv(factory);\n\n // First configured entry wins; the colliding second is dropped, not silently overwritten.\n models.Select(m => m.ModelName).Should().BeEquivalentTo([\"kimi-2.5\"]);\n models.Single().Id.Should().Be(\"kimi-2-5\");\n factory.Entries\n .Should()\n .ContainSingle(e => e.Level == LogLevel.Warning && e.Message.Contains(\"collides\") && e.Message.Contains(\"kimi-2-5\"));\n }\n\n [Fact]\n public void DiscoverFromEnv_DropsCrossFamilyIdCollision_KeepingFirstAndWarning()\n {\n // The same model name configured under two families generates the same id — a cross-family\n // collision. Keep the first family's entry and warn rather than silently routing the shared id\n // through the wrong family.\n using var _ = EnvScope.Set(\"ANTHROPIC_COMPAT_PROVIDERS\", \"DEEPSEEK,KIMI\");\n using var __ = EnvScope.Set(\"DEEPSEEK_ANTHROPIC_URL\", \"https://api.deepseek.com/anthropic\");\n using var ___ = EnvScope.Set(\"DEEPSEEK_APIKEY\", \"sk-deepseek\");\n using var ____ = EnvScope.Set(\"DEEPSEEK_MODELS\", \"shared-model\");\n using var _____ = EnvScope.Set(\"KIMI_ANTHROPIC_URL\", \"https://api.kimi.com/coding\");\n using var ______ = EnvScope.Set(\"KIMI_APIKEY\", \"sk-kimi\");\n using var _______ = EnvScope.Set(\"KIMI_MODELS\", \"shared-model\");\n\n var factory = new CapturingLoggerFactory();\n var models = AnthropicCompatProviders.DiscoverFromEnv(factory);\n\n var model = models.Should().ContainSingle().Subject;\n model.Id.Should().Be(\"shared-model\");\n model.FamilyKey.Should().Be(\"DEEPSEEK\");\n factory.Entries\n .Should()\n .ContainSingle(e => e.Level == LogLevel.Warning && e.Message.Contains(\"collides\") && e.Message.Contains(\"KIMI\"));\n }\n\n /// <summary>\n /// Minimal <see cref=\"ILoggerFactory\"/> capturing every entry across all created (named) loggers,\n /// so a test can assert the discovery path logs a clear collision warning. <c>DiscoverFromEnv</c>\n /// resolves a non-generic named logger, which the shared <c>CapturingLogger&lt;T&gt;</c> test double\n /// (an <c>ILogger&lt;T&gt;</c>) does not cover, so this small factory bridges the gap.\n /// </summary>\n private sealed class CapturingLoggerFactory : ILoggerFactory\n {\n private readonly List<(LogLevel Level, string Message)> _entries = [];\n\n public IReadOnlyList<(LogLevel Level, string Message)> Entries\n {\n get\n {\n lock (_entries)\n {\n return [.. _entries];\n }\n }\n }\n\n public ILogger CreateLogger(string categoryName) => new CapturingLogger(_entries);\n\n public void AddProvider(ILoggerProvider provider) { }\n\n public void Dispose() { }\n\n private sealed class CapturingLogger(List<(LogLevel Level, string Message)> entries) : ILogger\n {\n public IDisposable BeginScope<TState>(TState state)\n where TState : notnull => NullScope.Instance;\n\n public bool IsEnabled(LogLevel logLevel) => true;\n\n public void Log<TState>(\n LogLevel logLevel,\n EventId eventId,\n TState state,\n Exception? exception,\n Func<TState, Exception?, string> formatter)\n {\n var message = formatter(state, exception);\n lock (entries)\n {\n entries.Add((logLevel, message));\n }\n }\n\n private sealed class NullScope : IDisposable\n {\n public static readonly NullScope Instance = new();\n\n public void Dispose() { }\n }\n }\n }\n\n /// <summary>\n /// Snapshots the current value of an environment variable and restores it on dispose.\n /// Tests that mutate env vars must use this scope to avoid polluting other tests.\n /// </summary>\n private sealed class EnvScope : IDisposable\n {\n private readonly string _name;\n private readonly string? _previous;\n\n private EnvScope(string name, string? previous)\n {\n _name = name;\n _previous = previous;\n }\n\n public static EnvScope Set(string name, string? value)\n {\n var previous = Environment.GetEnvironmentVariable(name);\n Environment.SetEnvironmentVariable(name, value);\n return new EnvScope(name, previous);\n }\n\n public void Dispose()\n {\n Environment.SetEnvironmentVariable(_name, _previous);\n }\n }\n}\n"}}}}