DEV Community

vectronodeAPI
vectronodeAPI

Posted on

Your Agent's First Call Has a Byte Problem: UTF-8-Safe OpenAI-Compatible Requests

An AI Agent can pass every local test and still fail on its first real multilingual ticket. The Python client reads café and 上海 correctly, JavaScript returns a slightly different answer, and a PowerShell request produces replacement characters or an unexpected 400. Changing the model does not explain why the same prompt behaves differently in three clients.

The concrete problem in this article is easy to verify: make Python, JavaScript, and PowerShell construct the same 32-byte UTF-8 canary before an Agent sends its first OpenAI-compatible request. Each example checks a known SHA-256 value locally, sends JSON as explicit UTF-8 bytes, and accepts the model only if the response passes a small startup contract.

Disclosure: VectorNode is the gateway used for the examples. The byte-level checks belong to the application and work with other compatible endpoints as well.

1. The problem appears before the model starts reasoning

Suppose an Agent receives deployment tickets written in several languages. Its first call is a classification step, so a corrupted customer name or command can send the rest of the workflow down the wrong branch.

The failure is often misleading:

Python       -> ENCODING_OK
JavaScript   -> ENCODING_OK
PowerShell   -> ENCODING_MISMATCH
Enter fullscreen mode Exit fullscreen mode

All three clients may use the same key, endpoint, model, and visible prompt. They are not necessarily sending the same bytes. A source file may contain a byte-order mark, one script may build Windows-style CRLF line endings, or a client may convert a JSON string with an implicit legacy encoding.

Retrying the request does not repair those bytes. Switching models can make the symptom look intermittent because one model guesses the damaged text while another does not. The right first step is to prove the input before evaluating the output.

2. Why an OpenAI-compatible request can still differ by client

The official OpenAI Chat Completions reference defines POST /chat/completions with a model and a messages array. It also notes that parameter support can differ by model. The gateway's current interface and parameter documentation is available at https://o1kqetbjmk.apifox.cn.

That interface describes JSON values, not the hidden decisions made before the server parses them. Four layers matter:

  1. Characters: the application intends to send a Unicode string.
  2. Serialization: the object becomes JSON, including escaped newlines and quotes.
  3. Encoding: the JSON string becomes bytes, normally UTF-8.
  4. Transport metadata: Content-Type tells the receiver how to interpret those bytes.

The canary used here is:

Canary: café|上海|line1
line2
Enter fullscreen mode Exit fullscreen mode

It contains 27 Unicode characters but occupies 32 bytes in UTF-8. Its UTF-8 SHA-256 is:

266bb559203c3606b9f90cdec4f0187849c74cf415708d5ef2fd3349c85fe71b
Enter fullscreen mode Exit fullscreen mode

This fixed value turns an encoding suspicion into a local pass/fail check. It does not prove what a model understood, but it proves what the client prepared before serialization.

3. Configure one request contract

Create an account and local API key, then use one set of variable names in every client:

API_KEY=replace-with-a-real-key-locally
BASE_URL=https://www.vectronode.com/v1
MODEL=replace-with-a-model-id-visible-in-your-account
Enter fullscreen mode Exit fullscreen mode

Keep .env out of Git:

.env
Enter fullscreen mode Exit fullscreen mode

Install the Python dependencies and the small JavaScript environment loader. The JavaScript example uses the native fetch available in Node.js 18 or later.

py -m venv .venv
.\.venv\Scripts\python.exe -m pip install requests python-dotenv
npm install dotenv
Enter fullscreen mode Exit fullscreen mode

Python and JavaScript load .env directly. For PowerShell, copy the same non-secret values into process-scoped variables and enter the key without echoing it:

$env:BASE_URL = "copy-the-BASE_URL-value-from-your-local-env-file"
$env:MODEL = "copy-the-MODEL-value-from-your-local-env-file"
$secureKey = Read-Host "API key" -AsSecureString
$env:API_KEY = [Net.NetworkCredential]::new("", $secureKey).Password
Enter fullscreen mode Exit fullscreen mode

Do not manually set Content-Length. The HTTP clients calculate it from the final byte array. A character count is not a safe substitute.

4. Minimal runnable UTF-8 probes

Each program performs the same sequence:

  1. Construct the canary with explicit Unicode code points.
  2. Confirm 32 UTF-8 bytes and the expected SHA-256.
  3. Serialize a minimal Chat Completions body.
  4. Send the body as UTF-8 with charset=utf-8.
  5. Stop the Agent unless the response is exactly ENCODING_OK.

Python

Save as utf8_probe.py:

import hashlib
import json
import os

import requests
from dotenv import load_dotenv

load_dotenv()

required = ("API_KEY", "BASE_URL", "MODEL")
missing = [name for name in required if not os.getenv(name)]
if missing:
    raise RuntimeError(f"Missing environment variables: {', '.join(missing)}")

canary = "Canary: caf\u00e9|\u4e0a\u6d77|line1\nline2"
canary_bytes = canary.encode("utf-8")
canary_hash = hashlib.sha256(canary_bytes).hexdigest()
expected_hash = "266bb559203c3606b9f90cdec4f0187849c74cf415708d5ef2fd3349c85fe71b"

if len(canary_bytes) != 32 or canary_hash != expected_hash:
    raise RuntimeError("Local UTF-8 canary failed; no API request was sent.")

payload = {
    "model": os.environ["MODEL"],
    "messages": [
        {
            "role": "system",
            "content": (
                "Read the user message. If its accented text, CJK text, and "
                "two labeled lines are readable, return exactly ENCODING_OK. "
                "Otherwise return exactly ENCODING_MISMATCH."
            ),
        },
        {"role": "user", "content": canary},
    ],
    "temperature": 0,
    "max_tokens": 16,
}

body = json.dumps(
    payload,
    ensure_ascii=False,
    separators=(",", ":"),
).encode("utf-8")

response = requests.post(
    f"{os.environ['BASE_URL'].rstrip('/')}/chat/completions",
    headers={
        "Authorization": f"Bearer {os.environ['API_KEY']}",
        "Content-Type": "application/json; charset=utf-8",
    },
    data=body,
    timeout=60,
)

request_id = response.headers.get("x-request-id", "unavailable")
if not response.ok:
    raise RuntimeError(f"HTTP {response.status_code}; request_id={request_id}")

data = response.json()
choice = data["choices"][0]
content = choice["message"].get("content")
text = content.strip() if isinstance(content, str) else ""

result = {
    "ok": text == "ENCODING_OK",
    "canary_bytes": len(canary_bytes),
    "canary_sha256": canary_hash,
    "requested_model": os.environ["MODEL"],
    "reported_model": data.get("model"),
    "finish_reason": choice.get("finish_reason"),
    "request_id": request_id,
    "text": text,
}
print(json.dumps(result, indent=2, ensure_ascii=False))

if not result["ok"]:
    raise SystemExit("Agent startup probe failed.")
Enter fullscreen mode Exit fullscreen mode

Run it:

.\.venv\Scripts\python.exe .\utf8_probe.py
Enter fullscreen mode Exit fullscreen mode

JavaScript

Save as utf8_probe.mjs:

import "dotenv/config";
import { createHash } from "node:crypto";

const required = ["API_KEY", "BASE_URL", "MODEL"];
const missing = required.filter((name) => !process.env[name]);
if (missing.length) {
  throw new Error(`Missing environment variables: ${missing.join(", ")}`);
}

const canary = "Canary: caf\u00e9|\u4e0a\u6d77|line1\nline2";
const canaryBytes = Buffer.from(canary, "utf8");
const canaryHash = createHash("sha256").update(canaryBytes).digest("hex");
const expectedHash =
  "266bb559203c3606b9f90cdec4f0187849c74cf415708d5ef2fd3349c85fe71b";

if (canaryBytes.length !== 32 || canaryHash !== expectedHash) {
  throw new Error("Local UTF-8 canary failed; no API request was sent.");
}

const payload = {
  model: process.env.MODEL,
  messages: [
    {
      role: "system",
      content:
        "Read the user message. If its accented text, CJK text, and " +
        "two labeled lines are readable, return exactly ENCODING_OK. " +
        "Otherwise return exactly ENCODING_MISMATCH.",
    },
    { role: "user", content: canary },
  ],
  temperature: 0,
  max_tokens: 16,
};

const body = Buffer.from(JSON.stringify(payload), "utf8");
const response = await fetch(
  `${process.env.BASE_URL.replace(/\/$/, "")}/chat/completions`,
  {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.API_KEY}`,
      "Content-Type": "application/json; charset=utf-8",
    },
    body,
  },
);

const requestId = response.headers.get("x-request-id") ?? "unavailable";
if (!response.ok) {
  throw new Error(`HTTP ${response.status}; request_id=${requestId}`);
}

const data = await response.json();
const choice = data.choices?.[0];
const content = choice?.message?.content;
const text = typeof content === "string" ? content.trim() : "";
const result = {
  ok: text === "ENCODING_OK",
  canary_bytes: canaryBytes.length,
  canary_sha256: canaryHash,
  requested_model: process.env.MODEL,
  reported_model: data.model ?? null,
  finish_reason: choice?.finish_reason ?? null,
  request_id: requestId,
  text,
};

console.log(JSON.stringify(result, null, 2));
if (!result.ok) process.exitCode = 1;
Enter fullscreen mode Exit fullscreen mode

Run it:

node .\utf8_probe.mjs
Enter fullscreen mode Exit fullscreen mode

PowerShell

Save as utf8_probe.ps1. The canary is assembled from character codes, so the script itself remains ASCII-safe even in older Windows tooling.

$required = "API_KEY", "BASE_URL", "MODEL"
$missing = @(
    $required | Where-Object {
        -not [Environment]::GetEnvironmentVariable($_, "Process")
    }
)
if ($missing.Count -gt 0) {
    throw "Missing environment variables: $($missing -join ', ')"
}

$cafe = "caf$([char]0x00E9)"
$city = "$([char]0x4E0A)$([char]0x6D77)"
$canary = "Canary: $cafe|$city|line1`nline2"
$utf8 = [System.Text.UTF8Encoding]::new($false)
$canaryBytes = $utf8.GetBytes($canary)

$sha = [System.Security.Cryptography.SHA256]::Create()
try {
    $canaryHash = -join (
        $sha.ComputeHash($canaryBytes) |
            ForEach-Object { $_.ToString("x2") }
    )
} finally {
    $sha.Dispose()
}

$expectedHash = "266bb559203c3606b9f90cdec4f0187849c74cf415708d5ef2fd3349c85fe71b"
if ($canaryBytes.Length -ne 32 -or $canaryHash -ne $expectedHash) {
    throw "Local UTF-8 canary failed; no API request was sent."
}

$payload = [ordered]@{
    model = $env:MODEL
    messages = @(
        [ordered]@{
            role = "system"
            content = (
                "Read the user message. If its accented text, CJK text, and " +
                "two labeled lines are readable, return exactly ENCODING_OK. " +
                "Otherwise return exactly ENCODING_MISMATCH."
            )
        }
        [ordered]@{
            role = "user"
            content = $canary
        }
    )
    temperature = 0
    max_tokens = 16
}

$json = $payload | ConvertTo-Json -Depth 6 -Compress
$body = $utf8.GetBytes($json)
$headers = @{ Authorization = "Bearer $env:API_KEY" }
$uri = "$($env:BASE_URL.TrimEnd('/'))/chat/completions"

try {
    $response = Invoke-RestMethod `
        -Uri $uri `
        -Method Post `
        -Headers $headers `
        -ContentType "application/json; charset=utf-8" `
        -Body $body `
        -TimeoutSec 60
} catch {
    throw "Agent startup request failed: $($_.Exception.Message)"
}

$choice = $response.choices[0]
$text = ([string]$choice.message.content).Trim()
$result = [ordered]@{
    ok = ($text -eq "ENCODING_OK")
    canary_bytes = $canaryBytes.Length
    canary_sha256 = $canaryHash
    requested_model = $env:MODEL
    reported_model = $response.model
    finish_reason = $choice.finish_reason
    text = $text
}
$result | ConvertTo-Json

if (-not $result.ok) {
    exit 1
}
Enter fullscreen mode Exit fullscreen mode

Run it from the configured PowerShell process:

.\utf8_probe.ps1
Enter fullscreen mode Exit fullscreen mode

5. Verify the result before switching models

A successful first call should report the fixed local evidence and a passing model response:

{
  "ok": true,
  "canary_bytes": 32,
  "canary_sha256": "266bb559203c3606b9f90cdec4f0187849c74cf415708d5ef2fd3349c85fe71b",
  "requested_model": "model-id-from-your-account",
  "reported_model": "model-id-from-your-account",
  "finish_reason": "stop",
  "text": "ENCODING_OK"
}
Enter fullscreen mode Exit fullscreen mode

Interpret the evidence in order:

  1. A local byte or hash failure means the client did not construct the agreed canary. Stop before the network call.
  2. Matching local evidence proves the intended string, newline, and UTF-8 conversion inside that client.
  3. An HTTP success proves that the endpoint parsed and processed a request, but not that the model followed the probe instruction.
  4. ENCODING_OK is an application-level signal. A different answer needs diagnosis; it is not automatic proof of transport corruption.

Switch only MODEL when comparing two models:

$env:MODEL = "first-current-model-id"
.\.venv\Scripts\python.exe .\utf8_probe.py

$env:MODEL = "second-current-model-id"
.\.venv\Scripts\python.exe .\utf8_probe.py
Enter fullscreen mode Exit fullscreen mode

Run the same pair through JavaScript and PowerShell. Keep the canary, system instruction, endpoint, and parameters unchanged. Otherwise the test has more than one moving variable.

6. Diagnose failures without guessing

Symptom Likely layer First check
Local hash mismatch String construction or newline normalization Confirm one LF, the code points, 32 bytes, and the expected hash
400 invalid request JSON serialization or body encoding Inspect byte count and validate the redacted JSON locally
401 or 403 Authentication Confirm the process has the complete active key without printing it
404 URL construction Confirm one /v1 and one /chat/completions path segment
415 Media type Send application/json; charset=utf-8
model_not_found Model selection or account access Use an ID visible in the current account
422 Unsupported field or parameter Reduce the body to model and messages, then add fields individually
429 Rate, quota, or account limit Wait and inspect account status; do not create a tight retry loop
HTTP 200 with ENCODING_MISMATCH Model compliance or transport interpretation Re-run once, compare another model, and inspect only redacted diagnostics

Do not log the Authorization header or the complete response just to inspect an encoding error. Capture the HTTP status, request ID when available, selected model, canary hash, and byte counts. Those fields are usually enough to locate the failing layer.

Also avoid adding retries until the canary is stable. Repeating malformed bytes only produces more misleading logs and may trigger rate limits.

7. Security limits and appropriate use

Keep API keys in process-scoped environment variables, a local ignored .env, or a production secret manager. Never place a secret key in browser-side JavaScript, a mobile application, a repository, a screenshot, or a public issue. If a key is exposed, revoke and replace it; deleting the visible post is not sufficient.

Use synthetic text for a canary. A SHA-256 value is not anonymization: an attacker can guess and hash low-entropy prompts. Do not publish hashes of private customer text and do not include real names, tickets, source code, or credentials in an encoding probe.

This test is useful for multilingual agents, Windows automation, CI runners, and model-switch experiments that share a Chat Completions interface. It does not verify tool-call schemas, streaming event order, vision input, provider data retention, latency, or compliance requirements. Those need separate acceptance tests.

The most important boundary is simple: prove the bytes first, then evaluate the model. Otherwise a transport defect can be mistaken for an intelligence problem.

Try the example

After creating an account and selecting a current model ID, use the tested Python, JavaScript, and PowerShell examples in the VectorNode API Quickstart:

https://github.com/Vector-Compute-Engine/api-quickstart

Never publish your complete API key in source code, screenshots, videos, or public issues.

Top comments (0)