diff --git a/scripts/Update-Snapshots.ps1 b/scripts/Update-Snapshots.ps1 new file mode 100644 index 0000000..28be847 --- /dev/null +++ b/scripts/Update-Snapshots.ps1 @@ -0,0 +1,266 @@ +<# +.SYNOPSIS + Replays a snapshot case's request against the real DashScope API and + refreshes the three derived files (request.header.txt, response.header.txt, + response.body.txt). You only edit .request.body.json by hand. + +.DESCRIPTION + The test fixtures under test/Cnblogs.DashScope.Tests.Shared/RawHttpData/ + each consist of four files. The request body is hand-maintained; this script + re-sends it to the live DashScope API and regenerates the other three so the + fixtures stay consistent with what the SDK actually sends/expects. + + Authorization is taken ONLY from $env:DASHSCOPE_API_KEY (or -ApiKey), never + from the stale token inside the existing request.header.txt. + +.PARAMETER Case + One or more case names (without the .request.body.json suffix) to refresh. + Omit and leave -All unset to choose interactively from git-modified bodies. + +.PARAMETER All + Refresh every case that has a .request.body.json under -DataDir. + +.PARAMETER DataDir + RawHttpData directory. Defaults to test/Cnblogs.DashScope.Tests.Shared/RawHttpData + relative to the repo root (auto-located from this script's path). + +.PARAMETER RedactAuth + Write "Authorization: Bearer " into request.header.txt. Tests never + validate the token, so this is safe and avoids leaking keys. + +.PARAMETER ApiKey + Override $env:DASHSCOPE_API_KEY. + +.EXAMPLE + pwsh ./scripts/Update-Snapshots.ps1 + # interactively pick from git-modified *.request.body.json files + +.EXAMPLE + pwsh ./scripts/Update-Snapshots.ps1 -Case single-generation-message-sse -RedactAuth +#> +[CmdletBinding()] +param( + [Parameter(Position = 0, ValueFromRemainingArguments)] + [string[]]$Case, + + [switch]$All, + + [string]$DataDir, + + [switch]$RedactAuth, + + [string]$ApiKey +) + +$ErrorActionPreference = 'Stop' + +# ---------- locate repo root ---------- +$repoRoot = $PSScriptRoot +while ($repoRoot -and -not (Test-Path (Join-Path $repoRoot '.git'))) { + $parent = Split-Path $repoRoot -Parent + if ($parent -eq $repoRoot) { break } + $repoRoot = $parent +} +if (-not (Test-Path (Join-Path $repoRoot '.git'))) { + throw "Could not locate repo root (.git) from '$PSScriptRoot'. Pass -DataDir explicitly." +} + +if (-not $DataDir) { + $DataDir = Join-Path $repoRoot 'test/Cnblogs.DashScope.Tests.Shared/RawHttpData' +} +if (-not (Test-Path $DataDir)) { throw "DataDir not found: $DataDir" } + +# ---------- api key ---------- +if (-not $ApiKey) { $ApiKey = $env:DASHSCOPE_API_KEY } +if (-not $ApiKey) { + throw "No API key. Set `$env:DASHSCOPE_API_KEY or pass -ApiKey." +} + +# ---------- headers we never replay (HttpClient computes/host-specific) ---------- +$skipHeaders = @( + 'host', 'content-length', 'connection', 'accept-encoding', + 'postman-token', 'cache-control', 'user-agent', 'request-start-time' +) + +function Get-AbsoluteUrl { + param([string]$RequestLine, [hashtable]$Headers) + + # $RequestLine like "POST https://..." or "POST /path HTTP/1.1" + $parts = $RequestLine -split ' ', 3 + if ($parts.Count -lt 2) { throw "Cannot parse request line: '$RequestLine'" } + $url = $parts[1] + if ($url -match '^https?://') { return $url } + + # relative path -> combine with Host header + $hostName = $Headers['host'] + if (-not $hostName) { throw "Relative URL '$url' but no Host header." } + $scheme = 'https' + return "${scheme}://${hostName}${url}" +} + +function Parse-RequestHeader { + param([string]$Path) + $lines = Get-Content -LiteralPath $Path + $headers = @{} + for ($i = 1; $i -lt $lines.Count; $i++) { + $line = $lines[$i] + if ([string]::IsNullOrWhiteSpace($line)) { continue } + $idx = $line.IndexOf(':') + if ($idx -le 0) { continue } + $name = $line.Substring(0, $idx).Trim() + $value = $line.Substring($idx + 1).Trim() + $headers[$name] = $value + } + return [pscustomobject]@{ RequestLine = $lines[0]; Headers = $headers } +} + +function Select-CasesInteractive { + # Use git status to find changed request bodies. + $status = git -C $repoRoot status --porcelain (Join-Path $repoRoot '*' 2>$null) 2>$null + # The pathspec with repoRoot may be ignored; fall back to plain status. + if (-not $status) { $status = git -C $repoRoot status --porcelain } + $changed = @( + $status | ForEach-Object { + # porcelain format: "XY path" (path may be quoted for special chars) + $raw = $_ + # strip leading 2-char status + space + $p = $raw.Substring(3).Trim('"') + $p + } | Where-Object { $_ -match '\.request\.body\.json$' } + ) + + if (-not $changed) { + Write-Host "No modified *.request.body.json found by git. Pass -Case or -All." -ForegroundColor Yellow + exit 1 + } + + # normalize to case names, dedup + $names = @( + $changed | ForEach-Object { + $fn = Split-Path $_ -Leaf + $fn -replace '\.request\.body\.json$', '' + } | Sort-Object -Unique + ) + + Write-Host "`nModified request bodies:" -ForegroundColor Cyan + for ($i = 0; $i -lt $names.Count; $i++) { + Write-Host (" [{0,2}] {1}" -f $i, $names[$i]) + } + Write-Host "" + $answer = Read-Host "Enter indices (comma/space separated), 'all', or 'q' to quit" + $answer = $answer.Trim() + if (-not $answer -or $answer -eq 'q') { Write-Host "Cancelled."; exit 0 } + if ($answer -eq 'all') { return ,$names } + + $picked = @() + foreach ($tok in ($answer -split '[ ,]+') | Where-Object { $_ -ne '' }) { + if ($tok -match '^\d+$') { + $n = [int]$tok + if ($n -ge 0 -and $n -lt $names.Count) { $picked += $names[$n] } + else { Write-Host " ignoring out-of-range index $tok" -ForegroundColor Yellow } + } + } + if (-not $picked) { Write-Host "Nothing selected."; exit 0 } + return ,$picked +} + +function Resolve-Cases { + if ($All) { + $all = @(Get-ChildItem -LiteralPath $DataDir -Filter '*.request.body.json' | + ForEach-Object { $_.BaseName -replace '\.request\.body$', '' } | + Sort-Object -Unique) + if (-not $all) { Write-Host "No *.request.body.json in $DataDir" -ForegroundColor Yellow; exit 0 } + return ,$all + } + if ($Case -and $Case.Count) { return ,$Case } + return ,(Select-CasesInteractive) +} + +function Invoke-RefreshCase { + param([string]$Name) + + $bodyPath = Join-Path $DataDir "$Name.request.body.json" + $reqHeaderPath = Join-Path $DataDir "$Name.request.header.txt" + $respHeaderPath = Join-Path $DataDir "$Name.response.header.txt" + $respBodyPath = Join-Path $DataDir "$Name.response.body.txt" + + if (-not (Test-Path $bodyPath)) { + Write-Host " [skip] $Name : no .request.body.json (GET/multipart not supported)" -ForegroundColor Yellow + return + } + if (-not (Test-Path $reqHeaderPath)) { + Write-Host " [skip] $Name : no .request.header.txt" -ForegroundColor Yellow + return + } + + $parsed = Parse-RequestHeader -Path $reqHeaderPath + $method = ($parsed.RequestLine -split ' ', 2)[0] + $url = Get-AbsoluteUrl -RequestLine $parsed.RequestLine -Headers $parsed.Headers + + $body = Get-Content -LiteralPath $bodyPath -Raw + + # build outgoing request + $req = [System.Net.Http.HttpRequestMessage]::new([System.Net.Http.HttpMethod]::new($method), $url) + + $contentType = 'application/json' + $forwardHeaders = [ordered]@{} + foreach ($k in $parsed.Headers.Keys) { + if ($skipHeaders -contains $k.ToLower()) { continue } + if ($k -ieq 'content-type') { $contentType = $parsed.Headers[$k]; continue } + $forwardHeaders[$k] = $parsed.Headers[$k] + } + + $content = [System.Net.Http.StringContent]::new($body, [System.Text.Encoding]::UTF8, $contentType) + $req.Content = $content + + # Authorization always from env/key + $authValue = if ($RedactAuth) { 'Bearer ' } else { "Bearer $ApiKey" } + $req.Headers.TryAddWithoutValidation('Authorization', $authValue) | Out-Null + foreach ($k in $forwardHeaders.Keys) { + $req.Headers.TryAddWithoutValidation($k, $forwardHeaders[$k]) | Out-Null + } + + $resp = $httpClient.SendAsync($req, [System.Net.Http.HttpCompletionOption]::ResponseHeadersRead).GetAwaiter().GetResult() + $respBody = $resp.Content.ReadAsStringAsync().GetAwaiter().GetResult() + + # --- write request.header.txt (normalized) --- + $reqLines = [System.Collections.Generic.List[string]]::new() + $reqLines.Add("$method $url") + $reqLines.Add("Authorization: $authValue") + if ($contentType) { $reqLines.Add("Content-Type: $contentType") } + foreach ($k in $forwardHeaders.Keys) { $reqLines.Add("${k}: " + $forwardHeaders[$k]) } + [System.IO.File]::WriteAllLines($reqHeaderPath, $reqLines, (New-Object System.Text.UTF8Encoding $false)) + + # --- write response.header.txt --- + $respLines = [System.Collections.Generic.List[string]]::new() + $respLines.Add("HTTP/1.1 $([int]$resp.StatusCode) $($resp.ReasonPhrase)") + foreach ($h in $resp.Headers) { foreach ($v in $h.Value) { $respLines.Add("$($h.Key): $v") } } + foreach ($h in $resp.Content.Headers) { foreach ($v in $h.Value) { $respLines.Add("$($h.Key): $v") } } + [System.IO.File]::WriteAllLines($respHeaderPath, $respLines, (New-Object System.Text.UTF8Encoding $false)) + + # --- write response.body.txt --- + [System.IO.File]::WriteAllText($respBodyPath, $respBody, (New-Object System.Text.UTF8Encoding $false)) + + $mark = if ($resp.IsSuccessStatusCode) { '✓' } else { '✗' } + $color = if ($resp.IsSuccessStatusCode) { 'Green' } else { 'Red' } + Write-Host (" {0} {1} -> {2} {3}" -f $mark, $Name, [int]$resp.StatusCode, $resp.ReasonPhrase) -ForegroundColor $color +} + +# ---------- HttpClient with gzip/deflate auto-decompression ---------- +Add-Type -AssemblyName System.Net.Http -ErrorAction SilentlyContinue +$handler = [System.Net.Http.HttpClientHandler]::new() +$handler.AutomaticDecompression = [System.Net.DecompressionMethods]::GZip -bor ` + [System.Net.DecompressionMethods]::Deflate -bor [System.Net.DecompressionMethods]::Brotli +$httpClient = [System.Net.Http.HttpClient]::new($handler) +$httpClient.Timeout = [TimeSpan]::FromMinutes(5) + +try { + $cases = Resolve-Cases + Write-Host "Refreshing $($cases.Count) case(s) against live DashScope API..." -ForegroundColor Cyan + foreach ($c in $cases) { Invoke-RefreshCase -Name $c } + Write-Host "Done." -ForegroundColor Green +} +finally { + $httpClient.Dispose() + $handler.Dispose() +} diff --git a/src/Cnblogs.DashScope.AI/DashScopeChatClient.cs b/src/Cnblogs.DashScope.AI/DashScopeChatClient.cs index 90168f9..ff5b724 100644 --- a/src/Cnblogs.DashScope.AI/DashScopeChatClient.cs +++ b/src/Cnblogs.DashScope.AI/DashScopeChatClient.cs @@ -442,35 +442,8 @@ private MultimodalParameters ToMultimodalParameters(ChatOptions? options) return raw; } - var parameters = new MultimodalParameters - { - ResponseFormat = ToDashScopeResponseFormat(options.ResponseFormat), - Temperature = options.Temperature, - MaxTokens = options.MaxOutputTokens, - TopP = options.TopP, - TopK = options.TopK, - RepetitionPenalty = options.FrequencyPenalty, - PresencePenalty = options.PresencePenalty, - Seed = (ulong?)options.Seed, - Stop = options.StopSequences == null ? null : new TextGenerationStop(options.StopSequences), - Tools = options.Tools == null ? null : ToToolDefinitions(options.Tools), - ToolChoice = options.ToolMode switch - { - AutoChatToolMode => ToolChoice.AutoChoice, - RequiredChatToolMode required when string.IsNullOrEmpty(required.RequiredFunctionName) == false => - ToolChoice.FunctionChoice(required.RequiredFunctionName), - null => null, - _ => ToolChoice.AutoChoice - }, - ParallelToolCalls = options.AllowMultipleToolCalls, - EnableThinking = options.Reasoning?.Effort switch - { - null => null, - ReasoningEffort.None => false, - _ => true - } - }; - + var parameters = new MultimodalParameters(); + MapChatOptions(parameters, options); return parameters; } @@ -635,35 +608,98 @@ private static TextGenerationParameters ToTextGenerationParameters(ChatOptions? return parameters; } - return new TextGenerationParameters - { - ResultFormat = "message", - ResponseFormat = ToDashScopeResponseFormat(options.ResponseFormat), - Temperature = options.Temperature, - MaxTokens = options.MaxOutputTokens, - TopP = options.TopP, - TopK = options.TopK, - RepetitionPenalty = options.FrequencyPenalty, - PresencePenalty = options.PresencePenalty, - Seed = options.Seed == null ? null : (ulong)options.Seed.Value, - Stop = options.StopSequences == null ? null : new TextGenerationStop(options.StopSequences), - Tools = options.Tools == null ? null : ToToolDefinitions(options.Tools), - ToolChoice = options.ToolMode switch - { - AutoChatToolMode => ToolChoice.AutoChoice, - RequiredChatToolMode required when string.IsNullOrEmpty(required.RequiredFunctionName) == false => - ToolChoice.FunctionChoice(required.RequiredFunctionName), - null => null, - _ => ToolChoice.AutoChoice - }, - ParallelToolCalls = options.AllowMultipleToolCalls, - EnableThinking = options.Reasoning?.Effort switch - { - null => null, - ReasoningEffort.None => false, - _ => true - } + parameters = new TextGenerationParameters { ResultFormat = "message" }; + MapChatOptions(parameters, options); + return parameters; + } + + /// + /// Maps shared onto the parameter interfaces implemented by both + /// and . + /// + private static void MapChatOptions(TParameters parameters, ChatOptions options) + where TParameters : IProbabilityParameter, + IPenaltyParameter, + IMaxTokenParameter, + IStopTokenParameter, + ISeedParameter, + IStructuredOutputParameter, + IThinkingParameter, + IFunctionCallParameter + { + MapFromChatOptions((IProbabilityParameter)parameters, options); + MapFromChatOptions((IPenaltyParameter)parameters, options); + MapFromChatOptions((IMaxTokenParameter)parameters, options); + MapFromChatOptions((IStopTokenParameter)parameters, options); + MapFromChatOptions((ISeedParameter)parameters, options); + MapFromChatOptions((IStructuredOutputParameter)parameters, options); + MapFromChatOptions((IThinkingParameter)parameters, options); + MapFromChatOptions((IFunctionCallParameter)parameters, options); + } + + private static void MapFromChatOptions(IProbabilityParameter parameters, ChatOptions options) + { + parameters.Temperature = options.Temperature; + parameters.TopP = options.TopP; + parameters.TopK = options.TopK; + } + + private static void MapFromChatOptions(IPenaltyParameter parameters, ChatOptions options) + { + parameters.RepetitionPenalty = options.FrequencyPenalty; + parameters.PresencePenalty = options.PresencePenalty; + } + + private static void MapFromChatOptions(IMaxTokenParameter parameters, ChatOptions options) + { + parameters.MaxCompletionTokens = options.MaxOutputTokens; + } + + private static void MapFromChatOptions(IStopTokenParameter parameters, ChatOptions options) + { + parameters.Stop = options.StopSequences == null ? null : new TextGenerationStop(options.StopSequences); + } + + private static void MapFromChatOptions(ISeedParameter parameters, ChatOptions options) + { + parameters.Seed = (ulong?)options.Seed; + } + + private static void MapFromChatOptions(IStructuredOutputParameter parameters, ChatOptions options) + { + parameters.ResponseFormat = ToDashScopeResponseFormat(options.ResponseFormat); + } + + private static void MapFromChatOptions(IThinkingParameter parameters, ChatOptions options) + { + parameters.EnableThinking = options.Reasoning?.Effort switch + { + null => null, + ReasoningEffort.None => false, + _ => true + }; + parameters.ReasoningEffort = options.Reasoning?.Effort switch + { + ReasoningEffort.Low => "low", + ReasoningEffort.Medium => "medium", + ReasoningEffort.High => "high", + ReasoningEffort.ExtraHigh => "xhigh", + _ => null + }; + } + + private static void MapFromChatOptions(IFunctionCallParameter parameters, ChatOptions options) + { + parameters.Tools = options.Tools == null ? null : ToToolDefinitions(options.Tools); + parameters.ToolChoice = options.ToolMode switch + { + AutoChatToolMode => ToolChoice.AutoChoice, + RequiredChatToolMode required when string.IsNullOrEmpty(required.RequiredFunctionName) == false => + ToolChoice.FunctionChoice(required.RequiredFunctionName), + null => null, + _ => ToolChoice.AutoChoice }; + parameters.ParallelToolCalls = options.AllowMultipleToolCalls; } private static DashScopeResponseFormat? ToDashScopeResponseFormat(ChatResponseFormat? format) diff --git a/src/Cnblogs.DashScope.Core/ICodeInterpreterParameter.cs b/src/Cnblogs.DashScope.Core/ICodeInterpreterParameter.cs index c83501e..35f418a 100644 --- a/src/Cnblogs.DashScope.Core/ICodeInterpreterParameter.cs +++ b/src/Cnblogs.DashScope.Core/ICodeInterpreterParameter.cs @@ -8,5 +8,5 @@ public interface ICodeInterpreterParameter /// /// Allow model to call internal Python interpreter. Can not use with tools. /// - bool? EnableCodeInterpreter { get; } + bool? EnableCodeInterpreter { get; set; } } diff --git a/src/Cnblogs.DashScope.Core/IFunctionCallParameter.cs b/src/Cnblogs.DashScope.Core/IFunctionCallParameter.cs index 04b98dc..585d10c 100644 --- a/src/Cnblogs.DashScope.Core/IFunctionCallParameter.cs +++ b/src/Cnblogs.DashScope.Core/IFunctionCallParameter.cs @@ -8,15 +8,15 @@ public interface IFunctionCallParameter /// /// Available tools for model to call. /// - IEnumerable? Tools { get; } + IEnumerable? Tools { get; set; } /// /// Behavior when choosing tools. /// - ToolChoice? ToolChoice { get; } + ToolChoice? ToolChoice { get; set; } /// /// Whether to enable parallel tool calling /// - bool? ParallelToolCalls { get; } + bool? ParallelToolCalls { get; set; } } diff --git a/src/Cnblogs.DashScope.Core/IImageSynthesisParameters.cs b/src/Cnblogs.DashScope.Core/IImageSynthesisParameters.cs index 1b3585a..e45aa96 100644 --- a/src/Cnblogs.DashScope.Core/IImageSynthesisParameters.cs +++ b/src/Cnblogs.DashScope.Core/IImageSynthesisParameters.cs @@ -8,25 +8,25 @@ public interface IImageSynthesisParameters : ISeedParameter /// /// Generated image style, defaults to '<auto>'. Use to get all available options. /// - string? Style { get; } + string? Style { get; set; } /// /// Generated image size, defaults to 1024*1024. Another options are: 1280*720 and 720*1280. /// - string? Size { get; } + string? Size { get; set; } /// /// Number of images requested. Max number is 4, defaults to 1. /// - int? N { get; } + int? N { get; set; } /// /// Let LLM rewrite your positive prompt, Defaults to true. /// - bool? PromptExtend { get; } + bool? PromptExtend { get; set; } /// /// Adds AI-Generated watermark on bottom right corner. /// - bool? Watermark { get; } + bool? Watermark { get; set; } } diff --git a/src/Cnblogs.DashScope.Core/IIncrementalOutputParameter.cs b/src/Cnblogs.DashScope.Core/IIncrementalOutputParameter.cs index 66ba5a0..0f28298 100644 --- a/src/Cnblogs.DashScope.Core/IIncrementalOutputParameter.cs +++ b/src/Cnblogs.DashScope.Core/IIncrementalOutputParameter.cs @@ -8,5 +8,5 @@ public interface IIncrementalOutputParameter /// /// Enable stream output. Defaults to false. /// - public bool? IncrementalOutput { get; } + bool? IncrementalOutput { get; set; } } diff --git a/src/Cnblogs.DashScope.Core/IMaxTokenParameter.cs b/src/Cnblogs.DashScope.Core/IMaxTokenParameter.cs index eb89dae..c0e04ed 100644 --- a/src/Cnblogs.DashScope.Core/IMaxTokenParameter.cs +++ b/src/Cnblogs.DashScope.Core/IMaxTokenParameter.cs @@ -8,8 +8,11 @@ public interface IMaxTokenParameter /// /// The maximum number of tokens the model can generate. /// - /// - /// Default and maximum number of tokens is 1500(qwen-turbo) or 2000(qwen-max, qwen-max-1201, qwen-max-longcontext, qwen-plus). - /// - public int? MaxTokens { get; } + [Obsolete("Use MaxCompletionTokens instead")] + int? MaxTokens { get; set; } + + /// + /// The maximum number of tokens the model can generate, include reasoning output. + /// + int? MaxCompletionTokens { get; set; } } diff --git a/src/Cnblogs.DashScope.Core/IMultimodalParameters.cs b/src/Cnblogs.DashScope.Core/IMultimodalParameters.cs index 0b1982e..6cc069c 100644 --- a/src/Cnblogs.DashScope.Core/IMultimodalParameters.cs +++ b/src/Cnblogs.DashScope.Core/IMultimodalParameters.cs @@ -5,7 +5,6 @@ /// public interface IMultimodalParameters : IProbabilityParameter, - ISeedParameter, IIncrementalOutputParameter, IPenaltyParameter, IMaxTokenParameter, @@ -15,25 +14,26 @@ public interface IMultimodalParameters IStructuredOutputParameter, IWebSearchParameter, ICodeInterpreterParameter, - IImageSynthesisParameters + IImageSynthesisParameters, + IToolParameter { /// /// Allow higher resolution for inputs. When setting to true, increases the maximum input token from 1280 to 16384. Defaults to false. /// - bool? VlHighResolutionImages { get; } + bool? VlHighResolutionImages { get; set; } /// /// Options for speech recognition. /// - AsrOptions? AsrOptions { get; } + AsrOptions? AsrOptions { get; set; } /// /// Extra configurations for OCR models. /// - MultimodalOcrOptions? OcrOptions { get; } + MultimodalOcrOptions? OcrOptions { get; set; } /// /// Negative prompt when doing image generation task. /// - string? NegativePrompt { get; } + string? NegativePrompt { get; set; } } diff --git a/src/Cnblogs.DashScope.Core/IPenaltyParameter.cs b/src/Cnblogs.DashScope.Core/IPenaltyParameter.cs index d00a6e3..4efa9bf 100644 --- a/src/Cnblogs.DashScope.Core/IPenaltyParameter.cs +++ b/src/Cnblogs.DashScope.Core/IPenaltyParameter.cs @@ -8,11 +8,11 @@ public interface IPenaltyParameter /// /// Increasing the repetition penalty can reduce the amount of repetition in the model’s output. A value of 1.0 indicates no penalty, with the default set at 1.1. /// - public float? RepetitionPenalty { get; } + public float? RepetitionPenalty { get; set; } /// /// Control the content repetition in text generated by the model. /// /// Must be in [-2.0, 2.0]. Use higher penalty for batter creativity. - public float? PresencePenalty { get; } + public float? PresencePenalty { get; set; } } diff --git a/src/Cnblogs.DashScope.Core/IProbabilityParameter.cs b/src/Cnblogs.DashScope.Core/IProbabilityParameter.cs index 0efa681..77237b9 100644 --- a/src/Cnblogs.DashScope.Core/IProbabilityParameter.cs +++ b/src/Cnblogs.DashScope.Core/IProbabilityParameter.cs @@ -8,18 +8,18 @@ public interface IProbabilityParameter /// /// The probability threshold during generation, defaults to 0.8 when null. /// - public float? TopP { get; } + public float? TopP { get; set; } /// /// The size of the candidate set for sampling during generation. /// A larger value increases the randomness of the generated results. /// /// top_k would been disabled if applied null or any value larger than 100. - public int? TopK { get; } + public int? TopK { get; set; } /// /// Controls the diversity of generations. Lower temperature leads to more consistent result. /// /// Must be in [0,2), qwen-max defaults to 0.7. - public float? Temperature { get; } + public float? Temperature { get; set; } } diff --git a/src/Cnblogs.DashScope.Core/ISeedParameter.cs b/src/Cnblogs.DashScope.Core/ISeedParameter.cs index f1f377b..23134da 100644 --- a/src/Cnblogs.DashScope.Core/ISeedParameter.cs +++ b/src/Cnblogs.DashScope.Core/ISeedParameter.cs @@ -8,5 +8,5 @@ public interface ISeedParameter /// /// The seed for randomizer, defaults to 1234 when null. /// - public ulong? Seed { get; } + public ulong? Seed { get; set; } } diff --git a/src/Cnblogs.DashScope.Core/IStopTokenParameter.cs b/src/Cnblogs.DashScope.Core/IStopTokenParameter.cs index 793f6a0..1dbf7e8 100644 --- a/src/Cnblogs.DashScope.Core/IStopTokenParameter.cs +++ b/src/Cnblogs.DashScope.Core/IStopTokenParameter.cs @@ -8,5 +8,5 @@ public interface IStopTokenParameter /// /// Stop generation when next token or string is in given range. /// - public TextGenerationStop? Stop { get; } + public TextGenerationStop? Stop { get; set; } } diff --git a/src/Cnblogs.DashScope.Core/IStructuredOutputParameter.cs b/src/Cnblogs.DashScope.Core/IStructuredOutputParameter.cs index ebb8144..450797a 100644 --- a/src/Cnblogs.DashScope.Core/IStructuredOutputParameter.cs +++ b/src/Cnblogs.DashScope.Core/IStructuredOutputParameter.cs @@ -17,5 +17,5 @@ public interface IStructuredOutputParameter /// parameter.ResponseFormat = DashScopeResponseFormat.Json; /// /// - DashScopeResponseFormat? ResponseFormat { get; } + DashScopeResponseFormat? ResponseFormat { get; set; } } diff --git a/src/Cnblogs.DashScope.Core/ITextGenerationParameters.cs b/src/Cnblogs.DashScope.Core/ITextGenerationParameters.cs index f560590..fec42e9 100644 --- a/src/Cnblogs.DashScope.Core/ITextGenerationParameters.cs +++ b/src/Cnblogs.DashScope.Core/ITextGenerationParameters.cs @@ -14,7 +14,8 @@ public interface ITextGenerationParameters IFunctionCallParameter, IStructuredOutputParameter, IWebSearchParameter, - ICodeInterpreterParameter + ICodeInterpreterParameter, + IToolParameter { /// /// The format of the result, must be text or message. @@ -29,7 +30,7 @@ public interface ITextGenerationParameters /// parameter.ResultFormat = ResultFormats.Message; /// /// - string? ResultFormat { get; } + string? ResultFormat { get; set; } /// /// Include log possibilities in response. diff --git a/src/Cnblogs.DashScope.Core/IThinkingParameter.cs b/src/Cnblogs.DashScope.Core/IThinkingParameter.cs index 694379f..99dbde7 100644 --- a/src/Cnblogs.DashScope.Core/IThinkingParameter.cs +++ b/src/Cnblogs.DashScope.Core/IThinkingParameter.cs @@ -8,7 +8,7 @@ public interface IThinkingParameter /// /// Thinking option. Valid for supported models.(e.g. qwen3) /// - bool? EnableThinking { get; } + bool? EnableThinking { get; set; } /// /// Maximum length of thinking content. Valid for supported models.(e.g. qwen3) @@ -19,4 +19,9 @@ public interface IThinkingParameter /// Make past reasoning content in the chat history visible to the LLM. /// bool? PreserveThinking { get; set; } + + /// + /// Controls the reasoning effort for deepseek-v4 series models, "low", "medium", "high" equals to "high", "xhigh" equals to "max". + /// + string? ReasoningEffort { get; set; } } diff --git a/src/Cnblogs.DashScope.Core/IToolParameter.cs b/src/Cnblogs.DashScope.Core/IToolParameter.cs new file mode 100644 index 0000000..9291f18 --- /dev/null +++ b/src/Cnblogs.DashScope.Core/IToolParameter.cs @@ -0,0 +1,12 @@ +namespace Cnblogs.DashScope.Core; + +/// +/// Parameters for tool calls behavior. +/// +public interface IToolParameter +{ + /// + /// Streaming tool calls that with object or array type parameters. + /// + bool? ToolStream { get; set; } +} diff --git a/src/Cnblogs.DashScope.Core/IWebSearchParameter.cs b/src/Cnblogs.DashScope.Core/IWebSearchParameter.cs index 5aa8959..928a761 100644 --- a/src/Cnblogs.DashScope.Core/IWebSearchParameter.cs +++ b/src/Cnblogs.DashScope.Core/IWebSearchParameter.cs @@ -8,15 +8,15 @@ public interface IWebSearchParameter /// /// Enable internet search when generation. Defaults to false. /// - bool? EnableSearch { get; } + bool? EnableSearch { get; set; } /// /// Include <img> tags in the response. /// - bool? EnableTextImageMixed { get; } + bool? EnableTextImageMixed { get; set; } /// /// Search options. should set to true. /// - SearchOptions? SearchOptions { get; } + SearchOptions? SearchOptions { get; set; } } diff --git a/src/Cnblogs.DashScope.Core/MultimodalParameters.cs b/src/Cnblogs.DashScope.Core/MultimodalParameters.cs index 4863097..b8a6c4d 100644 --- a/src/Cnblogs.DashScope.Core/MultimodalParameters.cs +++ b/src/Cnblogs.DashScope.Core/MultimodalParameters.cs @@ -50,6 +50,9 @@ public class MultimodalParameters : IMultimodalParameters /// public int? MaxTokens { get; set; } + /// + public int? MaxCompletionTokens { get; set; } + /// public TextGenerationStop? Stop { get; set; } @@ -62,6 +65,9 @@ public class MultimodalParameters : IMultimodalParameters /// public bool? PreserveThinking { get; set; } + /// + public string? ReasoningEffort { get; set; } + /// public MultimodalOcrOptions? OcrOptions { get; set; } @@ -91,4 +97,7 @@ public class MultimodalParameters : IMultimodalParameters /// public bool? EnableCodeInterpreter { get; set; } + + /// + public bool? ToolStream { get; set; } } diff --git a/src/Cnblogs.DashScope.Core/TextGenerationParameters.cs b/src/Cnblogs.DashScope.Core/TextGenerationParameters.cs index 34eaaa5..bd1724c 100644 --- a/src/Cnblogs.DashScope.Core/TextGenerationParameters.cs +++ b/src/Cnblogs.DashScope.Core/TextGenerationParameters.cs @@ -17,6 +17,9 @@ public class TextGenerationParameters : ITextGenerationParameters /// public int? MaxTokens { get; set; } + /// + public int? MaxCompletionTokens { get; set; } + /// public float? TopP { get; set; } @@ -53,6 +56,9 @@ public class TextGenerationParameters : ITextGenerationParameters /// public bool? PreserveThinking { get; set; } + /// + public string? ReasoningEffort { get; set; } + /// public bool? Logprobs { get; set; } @@ -85,4 +91,7 @@ public class TextGenerationParameters : ITextGenerationParameters /// public bool? IncrementalOutput { get; set; } + + /// + public bool? ToolStream { get; set; } } diff --git a/test/Cnblogs.DashScope.AI.UnitTests/ChatClientTests.cs b/test/Cnblogs.DashScope.AI.UnitTests/ChatClientTests.cs index e393f54..04fd4b2 100644 --- a/test/Cnblogs.DashScope.AI.UnitTests/ChatClientTests.cs +++ b/test/Cnblogs.DashScope.AI.UnitTests/ChatClientTests.cs @@ -70,7 +70,7 @@ public async Task ChatClient_TextCompletion_SuccessAsync() FrequencyPenalty = parameter?.RepetitionPenalty, PresencePenalty = parameter?.PresencePenalty, ModelId = testCase.RequestModel.Model, - MaxOutputTokens = parameter?.MaxTokens, + MaxOutputTokens = parameter?.MaxCompletionTokens, Seed = (long?)parameter?.Seed, Temperature = parameter?.Temperature, TopK = parameter?.TopK, @@ -111,7 +111,7 @@ public async Task ChatClient_TextCompletionStream_SuccessAsync() FrequencyPenalty = parameter?.RepetitionPenalty, PresencePenalty = parameter?.PresencePenalty, ModelId = testCase.RequestModel.Model, - MaxOutputTokens = parameter?.MaxTokens, + MaxOutputTokens = parameter?.MaxCompletionTokens, Seed = (long?)parameter?.Seed, Temperature = parameter?.Temperature, TopK = parameter?.TopK, @@ -165,7 +165,7 @@ public async Task ChatClient_ImageRecognition_SuccessAsync() FrequencyPenalty = parameter?.RepetitionPenalty, PresencePenalty = parameter?.PresencePenalty, ModelId = testCase.RequestModel.Model, - MaxOutputTokens = parameter?.MaxTokens, + MaxOutputTokens = parameter?.MaxCompletionTokens, Seed = (long?)parameter?.Seed, Temperature = parameter?.Temperature, TopK = parameter?.TopK, @@ -211,7 +211,7 @@ public async Task ChatClient_ImageRecognitionStream_SuccessAsync() FrequencyPenalty = parameter?.RepetitionPenalty, PresencePenalty = parameter?.PresencePenalty, ModelId = testCase.RequestModel.Model, - MaxOutputTokens = parameter?.MaxTokens, + MaxOutputTokens = parameter?.MaxCompletionTokens, Seed = (long?)parameter?.Seed, Temperature = parameter?.Temperature, TopK = parameter?.TopK, @@ -261,7 +261,7 @@ public async Task ChatClient_TextCompletionStreamWithToolCalls_SuccessAsync() FrequencyPenalty = parameter?.RepetitionPenalty, PresencePenalty = parameter?.PresencePenalty, ModelId = testCase.Request.Model, - MaxOutputTokens = parameter?.MaxTokens, + MaxOutputTokens = parameter?.MaxCompletionTokens, Seed = (long?)parameter?.Seed, Temperature = parameter?.Temperature, TopK = parameter?.TopK, @@ -322,7 +322,7 @@ public async Task ChatClient_TextCompletionStreamWithToolMessages_SuccessAsync() FrequencyPenalty = parameter?.RepetitionPenalty, PresencePenalty = parameter?.PresencePenalty, ModelId = firstRound.Request.Model, - MaxOutputTokens = parameter?.MaxTokens, + MaxOutputTokens = parameter?.MaxCompletionTokens, Seed = (long?)parameter?.Seed, Temperature = parameter?.Temperature, TopK = parameter?.TopK, @@ -396,7 +396,7 @@ public async Task ChatClient_MultimodalStreamWithToolCalls_SuccessAsync() FrequencyPenalty = parameter?.RepetitionPenalty, PresencePenalty = parameter?.PresencePenalty, ModelId = testCase.Request.Model, - MaxOutputTokens = parameter?.MaxTokens, + MaxOutputTokens = parameter?.MaxCompletionTokens, Seed = (long?)parameter?.Seed, Temperature = parameter?.Temperature, TopK = parameter?.TopK, @@ -465,7 +465,7 @@ public async Task ChatClient_MultimodalStreamWithToolMessages_SuccessAsync() FrequencyPenalty = parameter?.RepetitionPenalty, PresencePenalty = parameter?.PresencePenalty, ModelId = firstRound.Request.Model, - MaxOutputTokens = parameter?.MaxTokens, + MaxOutputTokens = parameter?.MaxCompletionTokens, Seed = (long?)parameter?.Seed, Temperature = parameter?.Temperature, TopK = parameter?.TopK, diff --git a/test/Cnblogs.DashScope.Sdk.UnitTests/TextGenerationSerializationTests.cs b/test/Cnblogs.DashScope.Sdk.UnitTests/TextGenerationSerializationTests.cs index d6ab73e..a414fd9 100644 --- a/test/Cnblogs.DashScope.Sdk.UnitTests/TextGenerationSerializationTests.cs +++ b/test/Cnblogs.DashScope.Sdk.UnitTests/TextGenerationSerializationTests.cs @@ -241,6 +241,7 @@ public async Task ConversationCompletion_DeepResearchSse_SuccessAsync( ModelResponse>> SingleGenerationMessageFormatData = new( Snapshots.TextGeneration.MessageFormat.SingleMessage, Snapshots.TextGeneration.MessageFormat.SingleMessageReasoning, + Snapshots.TextGeneration.MessageFormat.SingleMessageReasoningEffort, Snapshots.TextGeneration.MessageFormat.SingleMessageWithTools, Snapshots.TextGeneration.MessageFormat.SingleMessageJson, Snapshots.TextGeneration.MessageFormat.SingleMessageLogprobs, diff --git a/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/multimodal-generation-vl-nosse.request.body.json b/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/multimodal-generation-vl-nosse.request.body.json index 0b0bb59..37bdb5f 100644 --- a/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/multimodal-generation-vl-nosse.request.body.json +++ b/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/multimodal-generation-vl-nosse.request.body.json @@ -25,7 +25,6 @@ "temperature": 1.1, "repetition_penalty": 1.3, "presence_penalty": 1.2, - "max_tokens": 120, "stop": "你好" } } diff --git a/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/single-generation-message-nosse.request.body.json b/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/single-generation-message-nosse.request.body.json index 7651470..85a905a 100644 --- a/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/single-generation-message-nosse.request.body.json +++ b/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/single-generation-message-nosse.request.body.json @@ -11,7 +11,7 @@ "parameters": { "result_format": "message", "seed": 1234, - "max_tokens": 1500, + "max_completion_tokens": 1500, "top_p": 0.8, "top_k": 100, "repetition_penalty": 1.1, diff --git a/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/single-generation-message-reasoning-effort-nosse.request.body.json b/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/single-generation-message-reasoning-effort-nosse.request.body.json new file mode 100644 index 0000000..9e44242 --- /dev/null +++ b/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/single-generation-message-reasoning-effort-nosse.request.body.json @@ -0,0 +1,16 @@ +{ + "model": "deepseek-v4-pro", + "input": { + "messages": [ + { + "role": "user", + "content": "填入数列“1,7,8,57,( ),3370”空缺处的数字" + } + ] + }, + "parameters": { + "incremental_output": false, + "enable_thinking": true, + "reasoning_effort": "xhigh" + } +} diff --git a/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/single-generation-message-reasoning-effort-nosse.request.header.txt b/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/single-generation-message-reasoning-effort-nosse.request.header.txt new file mode 100644 index 0000000..aa5748a --- /dev/null +++ b/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/single-generation-message-reasoning-effort-nosse.request.header.txt @@ -0,0 +1,5 @@ +POST https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation +Accept: application/json, text/plain, */* +Content-Type: application/json +User-Agent: bruno-runtime/3.4.2 +request-start-time: 1781358949976 diff --git a/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/single-generation-message-reasoning-effort-nosse.response.body.txt b/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/single-generation-message-reasoning-effort-nosse.response.body.txt new file mode 100644 index 0000000..d63440e --- /dev/null +++ b/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/single-generation-message-reasoning-effort-nosse.response.body.txt @@ -0,0 +1 @@ +{"output":{"choices":[{"finish_reason":"stop","message":{"content":"根据数列的规律,从第三项开始,每一项等于前第二项的平方加上前一项。具体验证如下:\n\n- 第三项:\\(1^2 + 7 = 8\\)\n- 第四项:\\(7^2 + 8 = 49 + 8 = 57\\)\n- 第五项:\\(8^2 + 57 = 64 + 57 = 121\\)\n- 第六项:\\(57^2 + 121 = 3249 + 121 = 3370\\)(与给定数列一致)\n\n因此,空缺处的数字应为 **121**。","reasoning_content":"我们被问到:\"填入数列“1,7,8,57,( ),3370”空缺处的数字\"。这是一个数列推理问题。我们需要找出模式。\n\n数列:1, 7, 8, 57, ?, 3370。\n\n让我们检查可能的模式。通常,数列可能涉及乘法、加法、平方或其他运算。\n\n观察数字:1, 7, 8, 57, ?, 3370。\n\n让我们看看相邻项之间的关系:\n\n从1到7:7 = 1 * ? 或者1 + 6?或者1^2 + 6?或者1 * 7?\n\n从7到8:8 = 7 + 1?或者7 * 1 + 1?\n\n从8到57:57 = 8 * 7 + 1?8*7=56,56+1=57。注意7是前一项?或者8 * 7 + 1,其中7是前两项?实际上,1,7,8: 8 = 1*7 + 1? 1*7+1=8。然后57 = 7*8 + 1? 7*8=56, 56+1=57。那么下一项可能是8*57 + 1? 8*57=456, 456+1=457。但最后一项是3370。457不等于3370。或者可能是8*57 + ? 8*57=456, 3370-456=2914,不是明显模式。\n\n另一种模式:也许涉及平方:7 = 2^3 - 1? 1^2+6? 不明显。\n\n检查:1, 7, 8, 57。1^2+7? 1+49=50? 不。\n\n也许模式是:a(n) = a(n-2) * a(n-1) + something。我们已经试过加1:1*7+1=8,7*8+1=57,那么8*57+1=457,但下一项是3370,不是457。如果我们继续:57*457+? 太大。\n\n也许模式不是加1,而是加一个递增的数?1*7+1=8,7*8+1=57,8*57+?=下一个。如果下一个是X,那么57*X+?=3370。或者模式可能是:a(n) = a(n-1)^2 + a(n-2)?7^2+1=50? 不。8^2+7=71? 不。57^2+8=3249+8=3257,不是3370。57^2+?=3370 -> 3370-3249=121,121=11^2。不显然。\n\n另一种思路:也许这些数字与某种函数有关,比如阶乘或指数。\n\n1, 7, 8, 57, ?, 3370。\n\n检查差值:6, 1, 49, ?, ?。1=1^2, 49=7^2? 6不是平方。\n\n也许模式是:a(n) = a(n-1) * a(n-2) + a(n-3)? 对于第四项:57 = 8*7 + 1? 8*7+1=57,是a(n-1)*a(n-2)+a(n-3)。那么第五项:? = 57*8 + 7 = 456+7=463。然后第六项:3370 = ?*57 + 8 = 463*57 + 8 = 26391+8=26399,不是3370。所以不是。\n\n也许:a(n) = a(n-1) * a(n-2) + a(n-1)? 8 = 7*1+1? 7*1+1=8,57 = 8*7+1? 8*7+1=57,但加的不是a(n-1)而是1?如果加的是a(n-2)? 8 = 7*1 + 1 (a(n-2)),57 = 8*7 + 1? 不,加的是a(n-3)? 57 = 8*7 + 1? 1是a(1)。那么下一项:? = 57*8 + 7 = 456+7=463,然后3370 = ?*57 + 8。如果?=463,463*57=26391,+8=26399。不对。\n\n也许模式是:a(n) = a(n-1)^2 + a(n-2)? 7^2+1=50,不。8^2+7=71,不。57^2+8=3257,不。\n\n也许模式涉及将数字视为字符串或数字操作?1,7,8,57: 1和7组成8?1和7拼在一起是17,不是8。7和8组成57?7和8拼成78?不是57。\n\n另一种想法:也许数列与递推关系有关,比如a(n) = p*a(n-1) + q*a(n-2)。对于三个已知项,我们可以解,但需要更多。\n\n让我们列出索引:n=1:1, n=2:7, n=3:8, n=4:57, n=5:?, n=6:3370。\n\n检查比率:7/1=7, 8/7≈1.14, 57/8=7.125, 3370/57≈59.12。没有明显模式。\n\n也许涉及乘法和加法与自身:7 = 1 * 7? 8 = 7 + 1? 57 = 8 * 7 + 1? 那么? = 57 * 8 + 7? 57*8=456, +7=463。然后3370 = ? * 57 + 8? 463*57=26391, +8=26399。不匹配。如果是? = 57 * 8 + 1? =457,那么3370 = 457 * 7? 457*7=3199,不。或者? = 57 * 7 + 8? =399+8=407? 57*7=399, +8=407。然后3370 = 407 * 8 + 57? 407*8=3256, +57=3313,不。407*8+? 不。\n\n也许模式是:a(n) = a(n-1) * a(n-2) + a(n-1) - something? \n\n另一种思路:也许这些数字是某个多项式序列的值?但通常这种智力题有简单模式。\n\n检查:1, 7, 8, 57, ?, 3370。注意7,8,57。8 = 2^3? 57 = 3^3? 3^3=27,不。7 = 2^3-1? 8=2^3? 57=4^3-7? 不。\n\n也许:1 = 1^2, 7 = 2^3-1? 8 = 2^3? 57 = 7^2+8? 7^2=49, 49+8=57。那么下一项:8^2+57? 64+57=121。然后121^2+? 或者57^2+121? 3249+121=3370! 等等,检查:57^2 + 121 = 3249 + 121 = 3370。是的!所以模式可能是:a(n) = a(n-2)^2 + a(n-1)? 让我们验证:\nn=3: a(1)^2 + a(2) = 1^2 + 7 = 1+7=8。正确。\nn=4: a(2)^2 + a(3) = 7^2 + 8 = 49+8=57。正确。\nn=5: a(3)^2 + a(4) = 8^2 + 57 = 64+57=121。正确。\nn=6: a(4)^2 + a(5) = 57^2 + 121 = 3249+121=3370。正确。\n\n所以空缺处的数字是121。\n\n验证整个序列:1, 7, 8, 57, 121, 3370。\n\n完美匹配。所以答案是121。","role":"assistant"}}]},"usage":{"input_tokens":102,"output_tokens":1941,"output_tokens_details":{"reasoning_tokens":1812},"prompt_tokens_details":{"cached_tokens":0},"total_tokens":2043},"request_id":"49319f28-a8ac-9ad2-8708-cb26014b083f"} diff --git a/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/single-generation-message-reasoning-effort-nosse.response.header.txt b/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/single-generation-message-reasoning-effort-nosse.response.header.txt new file mode 100644 index 0000000..53fc591 --- /dev/null +++ b/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/single-generation-message-reasoning-effort-nosse.response.header.txt @@ -0,0 +1,15 @@ +HTTP/1.1 200 OK +vary: Origin,Access-Control-Request-Method,Access-Control-Request-Headers, Accept-Encoding +content-type: application/json +x-request-id: f8543326-e333-98d9-a3cf-eb0f22e254b6 +x-dashscope-call-gateway: true +x-dashscope-finished: true +x-dashscope-timeout: 3600 +req-cost-time: 1480 +req-arrive-time: 1781358949396 +resp-start-time: 1781358950876 +x-envoy-upstream-service-time: 1473 +date: Sat, 13 Jun 2026 13:55:50 GMT +server: istio-envoy +transfer-encoding: chunked +request-duration: 1596 diff --git a/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/single-generation-message-reasoning-nosse.request.body.json b/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/single-generation-message-reasoning-nosse.request.body.json index 079da7b..a142c49 100644 --- a/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/single-generation-message-reasoning-nosse.request.body.json +++ b/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/single-generation-message-reasoning-nosse.request.body.json @@ -1,5 +1,5 @@ { - "model": "deepseek-r1", + "model": "deepseek-v4-pro", "input": { "messages": [ { @@ -10,6 +10,7 @@ }, "parameters": { "incremental_output": false, - "result_format": "message" + "enable_thinking": true, + "max_completion_tokens": 20 } } diff --git a/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/single-generation-message-reasoning-nosse.request.header.txt b/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/single-generation-message-reasoning-nosse.request.header.txt index 4e58a1c..c85e5d6 100644 --- a/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/single-generation-message-reasoning-nosse.request.header.txt +++ b/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/single-generation-message-reasoning-nosse.request.header.txt @@ -1,8 +1,5 @@ -POST /api/v1/services/aigc/text-generation/generation HTTP/1.1 +POST https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation +Accept: application/json, text/plain, */* Content-Type: application/json -Accept: */* -Cache-Control: no-cache -Host: dashscope.aliyuncs.com -Accept-Encoding: gzip, deflate, br -Connection: keep-alive -Content-Length: 273 +User-Agent: bruno-runtime/3.4.2 +request-start-time: 1781356696233 diff --git a/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/single-generation-message-reasoning-nosse.response.body.txt b/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/single-generation-message-reasoning-nosse.response.body.txt index 9bc4376..6ad64b0 100644 --- a/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/single-generation-message-reasoning-nosse.response.body.txt +++ b/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/single-generation-message-reasoning-nosse.response.body.txt @@ -1 +1 @@ -{"output":{"choices":[{"finish_reason":"stop","message":{"role":"assistant","content":"1 + 1 等于 **2**。这是基础的算术加法,当我们将一个单位与另一个单位相加时,总和为两个单位。","reasoning_content":"嗯,用户问1加1等于多少。这个问题看起来很简单,但可能有一些需要注意的地方。首先,我得确认用户是不是真的在问基本的数学问题,还是有其他的意图,比如测试我的反应或者开玩笑。\n\n1加1在基础算术里确实是2,但有时候可能会有不同的解释,比如在二进制中1+1等于10,或者在逻辑学中有时候表示为1,如果是布尔代数的话。不过通常情况下,用户可能只需要最直接的答案,也就是2。\n\n不过也有可能用户想考察我是否能够处理更复杂的情况,或者是否有隐藏的意思。比如,在某些情况下,1加1可能被用来比喻合作的效果,比如“1+1大于2”,但这可能超出了当前问题的范围。\n\n我需要考虑用户的背景。如果用户是小学生,那么直接回答2是正确的,并且可能需要鼓励的话。如果是成年人,可能还是同样的答案,但不需要额外的解释。如果用户来自数学或计算机领域,可能需要确认是否需要其他进制的答案,但通常默认是十进制。\n\n另外,检查是否有拼写错误或非阿拉伯数字的情况,比如罗马数字的I+I,但问题里明确写的是1+1,所以应该是阿拉伯数字。\n\n总结下来,最安全也是最正确的答案就是2。不过为了确保,可以简短地确认用户的意图,但按照常规问题处理,直接回答即可。"}}]},"usage":{"total_tokens":313,"output_tokens":302,"input_tokens":11},"request_id":"7039d8ff-89e0-9191-b4d3-0d258a7d70e1"} +{"output":{"choices":[{"finish_reason":"length","message":{"content":"","reasoning_content":"我们被问到:\"请问 1+1 是多少?\" 这是一个简单的问题。答案显然是","role":"assistant"}}]},"usage":{"input_tokens":12,"output_tokens":21,"output_tokens_details":{"reasoning_tokens":20},"prompt_tokens_details":{"cached_tokens":0},"total_tokens":33},"request_id":"7c8beccf-3e76-9034-95c1-db1f1ab9d6e0"} diff --git a/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/single-generation-message-reasoning-nosse.response.header.txt b/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/single-generation-message-reasoning-nosse.response.header.txt index 5767ccb..8f81514 100644 --- a/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/single-generation-message-reasoning-nosse.response.header.txt +++ b/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/single-generation-message-reasoning-nosse.response.header.txt @@ -1,15 +1,15 @@ HTTP/1.1 200 OK vary: Origin,Access-Control-Request-Method,Access-Control-Request-Headers, Accept-Encoding content-type: application/json -x-request-id: 7039d8ff-89e0-9191-b4d3-0d258a7d70e1 -x-dashscope-timeout: 180 +x-request-id: 7c8beccf-3e76-9034-95c1-db1f1ab9d6e0 x-dashscope-call-gateway: true x-dashscope-finished: true -req-cost-time: 11619 -req-arrive-time: 1742405583711 -resp-start-time: 1742405595330 -x-envoy-upstream-service-time: 11610 -content-encoding: gzip -date: Wed, 19 Mar 2025 17:33:15 GMT +x-dashscope-timeout: 1800 +req-cost-time: 1367 +req-arrive-time: 1781356754730 +resp-start-time: 1781356756098 +x-envoy-upstream-service-time: 1360 +date: Sat, 13 Jun 2026 13:19:15 GMT server: istio-envoy transfer-encoding: chunked +request-duration: 1483 diff --git a/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/single-generation-message-sse.request.body.json b/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/single-generation-message-sse.request.body.json index 6355ecf..63fa291 100644 --- a/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/single-generation-message-sse.request.body.json +++ b/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/single-generation-message-sse.request.body.json @@ -11,7 +11,7 @@ "parameters": { "result_format": "message", "seed": 1234, - "max_tokens": 1500, + "max_completion_tokens": 1500, "top_p": 0.8, "top_k": 100, "repetition_penalty": 1.1, diff --git a/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/single-generation-message-with-parallel-tools-sse.request.body.json b/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/single-generation-message-with-parallel-tools-sse.request.body.json index 540ce2c..034f5b5 100644 --- a/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/single-generation-message-with-parallel-tools-sse.request.body.json +++ b/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/single-generation-message-with-parallel-tools-sse.request.body.json @@ -11,7 +11,6 @@ "parameters": { "result_format": "message", "seed": 6999, - "max_tokens": 1500, "top_p": 0.8, "top_k": 100, "repetition_penalty": 1.1, diff --git a/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/single-generation-message-with-tools-nosse.request.body.json b/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/single-generation-message-with-tools-nosse.request.body.json index 7e41d14..6515ed4 100644 --- a/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/single-generation-message-with-tools-nosse.request.body.json +++ b/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/single-generation-message-with-tools-nosse.request.body.json @@ -11,7 +11,6 @@ "parameters": { "result_format": "message", "seed": 1234, - "max_tokens": 1500, "top_p": 0.8, "top_k": 100, "repetition_penalty": 1.1, diff --git a/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/single-generation-message-with-tools-sse.request.body.json b/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/single-generation-message-with-tools-sse.request.body.json index ac6f824..ae9afac 100644 --- a/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/single-generation-message-with-tools-sse.request.body.json +++ b/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/single-generation-message-with-tools-sse.request.body.json @@ -45,7 +45,6 @@ "parameters": { "result_format": "message", "seed": 6999, - "max_tokens": 1500, "incremental_output": true, "tools": [ { diff --git a/test/Cnblogs.DashScope.Tests.Shared/Utils/Checkers.cs b/test/Cnblogs.DashScope.Tests.Shared/Utils/Checkers.cs index c1c6d85..5194673 100644 --- a/test/Cnblogs.DashScope.Tests.Shared/Utils/Checkers.cs +++ b/test/Cnblogs.DashScope.Tests.Shared/Utils/Checkers.cs @@ -1,4 +1,5 @@ -using System.Text.Json.Nodes; +using System.Text.Json; +using System.Text.Json.Nodes; using Cnblogs.DashScope.Core.Internals; namespace Cnblogs.DashScope.Tests.Shared.Utils; @@ -60,19 +61,4 @@ public static bool IsJsonEquivalent(HttpContent content, string requestSnapshot) var expected = JsonNode.Parse(requestSnapshot); return JsonNode.DeepEquals(actual, expected); } - - public static bool IsFileUploaded(HttpContent? content, params string[] files) - { - if (content is not MultipartFormDataContent form) - { - return false; - } - - if (form.Count(x => x.GetType() == typeof(StreamContent)) != files.Length) - { - return false; - } - - return true; - } } diff --git a/test/Cnblogs.DashScope.Tests.Shared/Utils/Snapshots.MicrosoftExtensionsAi.cs b/test/Cnblogs.DashScope.Tests.Shared/Utils/Snapshots.MicrosoftExtensionsAi.cs index b611e92..0683210 100644 --- a/test/Cnblogs.DashScope.Tests.Shared/Utils/Snapshots.MicrosoftExtensionsAi.cs +++ b/test/Cnblogs.DashScope.Tests.Shared/Utils/Snapshots.MicrosoftExtensionsAi.cs @@ -23,7 +23,6 @@ public static readonly { ResultFormat = "message", Seed = 6999, - MaxTokens = 1500, IncrementalOutput = true, Tools = new List { @@ -211,7 +210,6 @@ public static readonly { ResultFormat = "message", Seed = 6999, - MaxTokens = 1500, IncrementalOutput = true, Tools = new List { diff --git a/test/Cnblogs.DashScope.Tests.Shared/Utils/Snapshots.MultimodalGeneration.cs b/test/Cnblogs.DashScope.Tests.Shared/Utils/Snapshots.MultimodalGeneration.cs index 69c589f..e50b85e 100644 --- a/test/Cnblogs.DashScope.Tests.Shared/Utils/Snapshots.MultimodalGeneration.cs +++ b/test/Cnblogs.DashScope.Tests.Shared/Utils/Snapshots.MultimodalGeneration.cs @@ -43,7 +43,6 @@ public static partial class MultimodalGeneration VlHighResolutionImages = true, RepetitionPenalty = 1.3f, PresencePenalty = 1.2f, - MaxTokens = 120, Stop = "你好" } }, @@ -95,7 +94,6 @@ public static partial class MultimodalGeneration Temperature = 1.1f, RepetitionPenalty = 1.3f, PresencePenalty = 1.2f, - MaxTokens = 120, } }, new ModelResponse diff --git a/test/Cnblogs.DashScope.Tests.Shared/Utils/Snapshots.TextGeneration.cs b/test/Cnblogs.DashScope.Tests.Shared/Utils/Snapshots.TextGeneration.cs index 5d9bded..35ca4c2 100644 --- a/test/Cnblogs.DashScope.Tests.Shared/Utils/Snapshots.TextGeneration.cs +++ b/test/Cnblogs.DashScope.Tests.Shared/Utils/Snapshots.TextGeneration.cs @@ -99,7 +99,7 @@ public static partial class MessageFormat { ResultFormat = "message", Seed = 1234, - MaxTokens = 1500, + MaxCompletionTokens = 1500, TopP = 0.8f, TopK = 100, RepetitionPenalty = 1.1f, @@ -139,7 +139,7 @@ public static partial class MessageFormat "single-generation-message-reasoning", new ModelRequest { - Model = "deepseek-r1", + Model = "deepseek-v4-pro", Input = new TextGenerationInput { @@ -148,7 +148,55 @@ public static partial class MessageFormat }, Parameters = new TextGenerationParameters { - IncrementalOutput = false, ResultFormat = ResultFormats.Message + IncrementalOutput = false, EnableThinking = true, MaxCompletionTokens = 20 + } + }, + new ModelResponse + { + Output = new TextGenerationOutput + { + Choices = + new List + { + new() + { + FinishReason = "length", + Message = + TextChatMessage.Assistant( + string.Empty, + null, + null, + "我们被问到:\"请问 1+1 是多少?\" 这是一个简单的问题。答案显然是") + } + } + }, + RequestId = "7c8beccf-3e76-9034-95c1-db1f1ab9d6e0", + Usage = new TextGenerationTokenUsage + { + TotalTokens = 33, + OutputTokens = 21, + InputTokens = 12, + PromptTokensDetails = new TextGenerationPromptTokenDetails(0), + OutputTokensDetails = new TextGenerationOutputTokenDetails(20) + } + }); + + public static readonly RequestSnapshot, + ModelResponse> + SingleMessageReasoningEffort = new( + "single-generation-message-reasoning-effort", + new ModelRequest + { + Model = "deepseek-v4-pro", + Input = + new TextGenerationInput + { + Messages = + new List { TextChatMessage.User("填入数列“1,7,8,57,( ),3370”空缺处的数字") } + }, + Parameters = new TextGenerationParameters + { + IncrementalOutput = false, EnableThinking = true, ReasoningEffort = "xhigh" } }, new ModelResponse @@ -163,19 +211,21 @@ public static partial class MessageFormat FinishReason = "stop", Message = TextChatMessage.Assistant( - "1 + 1 等于 **2**。这是基础的算术加法,当我们将一个单位与另一个单位相加时,总和为两个单位。", + "根据数列的规律,从第三项开始,每一项等于前第二项的平方加上前一项。具体验证如下:\n\n- 第三项:\\(1^2 + 7 = 8\\)\n- 第四项:\\(7^2 + 8 = 49 + 8 = 57\\)\n- 第五项:\\(8^2 + 57 = 64 + 57 = 121\\)\n- 第六项:\\(57^2 + 121 = 3249 + 121 = 3370\\)(与给定数列一致)\n\n因此,空缺处的数字应为 **121**。", null, null, - "嗯,用户问1加1等于多少。这个问题看起来很简单,但可能有一些需要注意的地方。首先,我得确认用户是不是真的在问基本的数学问题,还是有其他的意图,比如测试我的反应或者开玩笑。\n\n1加1在基础算术里确实是2,但有时候可能会有不同的解释,比如在二进制中1+1等于10,或者在逻辑学中有时候表示为1,如果是布尔代数的话。不过通常情况下,用户可能只需要最直接的答案,也就是2。\n\n不过也有可能用户想考察我是否能够处理更复杂的情况,或者是否有隐藏的意思。比如,在某些情况下,1加1可能被用来比喻合作的效果,比如“1+1大于2”,但这可能超出了当前问题的范围。\n\n我需要考虑用户的背景。如果用户是小学生,那么直接回答2是正确的,并且可能需要鼓励的话。如果是成年人,可能还是同样的答案,但不需要额外的解释。如果用户来自数学或计算机领域,可能需要确认是否需要其他进制的答案,但通常默认是十进制。\n\n另外,检查是否有拼写错误或非阿拉伯数字的情况,比如罗马数字的I+I,但问题里明确写的是1+1,所以应该是阿拉伯数字。\n\n总结下来,最安全也是最正确的答案就是2。不过为了确保,可以简短地确认用户的意图,但按照常规问题处理,直接回答即可。") + "我们被问到:\"填入数列“1,7,8,57,( ),3370”空缺处的数字\"。这是一个数列推理问题。我们需要找出模式。\n\n数列:1, 7, 8, 57, ?, 3370。\n\n让我们检查可能的模式。通常,数列可能涉及乘法、加法、平方或其他运算。\n\n观察数字:1, 7, 8, 57, ?, 3370。\n\n让我们看看相邻项之间的关系:\n\n从1到7:7 = 1 * ? 或者1 + 6?或者1^2 + 6?或者1 * 7?\n\n从7到8:8 = 7 + 1?或者7 * 1 + 1?\n\n从8到57:57 = 8 * 7 + 1?8*7=56,56+1=57。注意7是前一项?或者8 * 7 + 1,其中7是前两项?实际上,1,7,8: 8 = 1*7 + 1? 1*7+1=8。然后57 = 7*8 + 1? 7*8=56, 56+1=57。那么下一项可能是8*57 + 1? 8*57=456, 456+1=457。但最后一项是3370。457不等于3370。或者可能是8*57 + ? 8*57=456, 3370-456=2914,不是明显模式。\n\n另一种模式:也许涉及平方:7 = 2^3 - 1? 1^2+6? 不明显。\n\n检查:1, 7, 8, 57。1^2+7? 1+49=50? 不。\n\n也许模式是:a(n) = a(n-2) * a(n-1) + something。我们已经试过加1:1*7+1=8,7*8+1=57,那么8*57+1=457,但下一项是3370,不是457。如果我们继续:57*457+? 太大。\n\n也许模式不是加1,而是加一个递增的数?1*7+1=8,7*8+1=57,8*57+?=下一个。如果下一个是X,那么57*X+?=3370。或者模式可能是:a(n) = a(n-1)^2 + a(n-2)?7^2+1=50? 不。8^2+7=71? 不。57^2+8=3249+8=3257,不是3370。57^2+?=3370 -> 3370-3249=121,121=11^2。不显然。\n\n另一种思路:也许这些数字与某种函数有关,比如阶乘或指数。\n\n1, 7, 8, 57, ?, 3370。\n\n检查差值:6, 1, 49, ?, ?。1=1^2, 49=7^2? 6不是平方。\n\n也许模式是:a(n) = a(n-1) * a(n-2) + a(n-3)? 对于第四项:57 = 8*7 + 1? 8*7+1=57,是a(n-1)*a(n-2)+a(n-3)。那么第五项:? = 57*8 + 7 = 456+7=463。然后第六项:3370 = ?*57 + 8 = 463*57 + 8 = 26391+8=26399,不是3370。所以不是。\n\n也许:a(n) = a(n-1) * a(n-2) + a(n-1)? 8 = 7*1+1? 7*1+1=8,57 = 8*7+1? 8*7+1=57,但加的不是a(n-1)而是1?如果加的是a(n-2)? 8 = 7*1 + 1 (a(n-2)),57 = 8*7 + 1? 不,加的是a(n-3)? 57 = 8*7 + 1? 1是a(1)。那么下一项:? = 57*8 + 7 = 456+7=463,然后3370 = ?*57 + 8。如果?=463,463*57=26391,+8=26399。不对。\n\n也许模式是:a(n) = a(n-1)^2 + a(n-2)? 7^2+1=50,不。8^2+7=71,不。57^2+8=3257,不。\n\n也许模式涉及将数字视为字符串或数字操作?1,7,8,57: 1和7组成8?1和7拼在一起是17,不是8。7和8组成57?7和8拼成78?不是57。\n\n另一种想法:也许数列与递推关系有关,比如a(n) = p*a(n-1) + q*a(n-2)。对于三个已知项,我们可以解,但需要更多。\n\n让我们列出索引:n=1:1, n=2:7, n=3:8, n=4:57, n=5:?, n=6:3370。\n\n检查比率:7/1=7, 8/7≈1.14, 57/8=7.125, 3370/57≈59.12。没有明显模式。\n\n也许涉及乘法和加法与自身:7 = 1 * 7? 8 = 7 + 1? 57 = 8 * 7 + 1? 那么? = 57 * 8 + 7? 57*8=456, +7=463。然后3370 = ? * 57 + 8? 463*57=26391, +8=26399。不匹配。如果是? = 57 * 8 + 1? =457,那么3370 = 457 * 7? 457*7=3199,不。或者? = 57 * 7 + 8? =399+8=407? 57*7=399, +8=407。然后3370 = 407 * 8 + 57? 407*8=3256, +57=3313,不。407*8+? 不。\n\n也许模式是:a(n) = a(n-1) * a(n-2) + a(n-1) - something? \n\n另一种思路:也许这些数字是某个多项式序列的值?但通常这种智力题有简单模式。\n\n检查:1, 7, 8, 57, ?, 3370。注意7,8,57。8 = 2^3? 57 = 3^3? 3^3=27,不。7 = 2^3-1? 8=2^3? 57=4^3-7? 不。\n\n也许:1 = 1^2, 7 = 2^3-1? 8 = 2^3? 57 = 7^2+8? 7^2=49, 49+8=57。那么下一项:8^2+57? 64+57=121。然后121^2+? 或者57^2+121? 3249+121=3370! 等等,检查:57^2 + 121 = 3249 + 121 = 3370。是的!所以模式可能是:a(n) = a(n-2)^2 + a(n-1)? 让我们验证:\nn=3: a(1)^2 + a(2) = 1^2 + 7 = 1+7=8。正确。\nn=4: a(2)^2 + a(3) = 7^2 + 8 = 49+8=57。正确。\nn=5: a(3)^2 + a(4) = 8^2 + 57 = 64+57=121。正确。\nn=6: a(4)^2 + a(5) = 57^2 + 121 = 3249+121=3370。正确。\n\n所以空缺处的数字是121。\n\n验证整个序列:1, 7, 8, 57, 121, 3370。\n\n完美匹配。所以答案是121。") } } }, - RequestId = "7039d8ff-89e0-9191-b4d3-0d258a7d70e1", + RequestId = "49319f28-a8ac-9ad2-8708-cb26014b083f", Usage = new TextGenerationTokenUsage { - TotalTokens = 313, - OutputTokens = 302, - InputTokens = 11 + TotalTokens = 2043, + OutputTokens = 1941, + InputTokens = 102, + PromptTokensDetails = new TextGenerationPromptTokenDetails(0), + OutputTokensDetails = new TextGenerationOutputTokenDetails(1812) } }); @@ -196,7 +246,7 @@ public static partial class MessageFormat { ResultFormat = "message", Seed = 1234, - MaxTokens = 1500, + MaxCompletionTokens = 1500, TopP = 0.8f, TopK = 100, RepetitionPenalty = 1.1f, @@ -654,7 +704,7 @@ public static readonly { ResultFormat = "message", Seed = 1234, - MaxTokens = 1500, + MaxCompletionTokens = 1500, TopP = 0.8f, TopK = 100, RepetitionPenalty = 1.1f, @@ -754,7 +804,7 @@ public static readonly { ResultFormat = "message", Seed = 1234, - MaxTokens = 1500, + MaxCompletionTokens = 1500, TopP = 0.8f, TopK = 100, RepetitionPenalty = 1.1f, @@ -805,7 +855,6 @@ public static readonly { ResultFormat = "message", Seed = 1234, - MaxTokens = 1500, TopP = 0.8f, TopK = 100, RepetitionPenalty = 1.1f, @@ -879,7 +928,6 @@ public static readonly { ResultFormat = "message", Seed = 6999, - MaxTokens = 1500, TopP = 0.8f, TopK = 100, RepetitionPenalty = 1.1f, @@ -1047,7 +1095,6 @@ public static readonly { ResultFormat = "message", Seed = 6999, - MaxTokens = 1500, IncrementalOutput = true, Tools = new List {