Send the labels that make the reports and the traces show your tool, your sessions and your users. If your agent calls a model provider directly, send its OpenTelemetry metrics to the gateway.
What the gateway records
For each request: the route, the model, the latency, the token counts, the cost, the key id and the labels on this page.
Never the prompt text, the answer text or your key. An administrator can turn on body capture for a route. Ask which routes have it.
A label that is too long is dropped whole. The gateway never cuts a label, because a cut label would join two different things.
1. Identify the request
Send your client key in one of these headers. The reports attribute the request to the owner of the key. A key with no owner reports as a service account.
Field
Where
Meaning
Limit
In the reports
Authorization
header
Your client key, as Bearer <key>. The OpenAI style.
—
the key and its owner
x-api-key
header
Your client key. The Anthropic style, used by Claude Code and the Anthropic SDK.
—
the key and its owner
x-hooshyar-key
header
Your client key, only on the subscription lane and on the OpenTelemetry export, where the other two headers carry a different credential or none.
—
the key and its owner
2. Select the route
The route decides the model. The page Connect a client tells how to select one. /v1/classify tells which route a text goes to, and why, without a model call.
Field
Where
Meaning
Limit
In the reports
model
body
auto, or the name of a route.
—
route, model
x-task-type
header
The name of a route. Wins over model and over route.
—
route, and source = hint
route
body
The name of a route. The gateway removes this field before it forwards the request.
—
route, and source = hint
3. Label the request
These labels are optional. Each one adds a dimension to the reports. The gateway removes the x- headers before it forwards the request to the provider.
Field
Where
Meaning
Limit
In the reports
x-client
header
The name of your tool or application. Wins over the user agent. Use one short name per tool, always the same.
32 characters. A longer value is ignored, and the user agent decides.
the Client dimension: cost and users per tool
x-session-id
header
One id per run or per conversation. Send the same id on every request of the run, on every retry, and on /mcp.
64 characters. A longer value is not recorded.
the Session dimension: sessions, session hours, cost per session, the PR stamp
x-tags
header
Labels, comma separated: a team, an experiment, a feature. A repeated label is stored once.
—
the Tag dimension. A guardrail rule can select a tag.
user
body
The person the application acts for. The OpenAI field.
—
the User dimension, beside the key owner
metadata.user_id
body
The same, in the Anthropic Messages API. Claude Code sends it with its session id inside.
—
the User dimension, and the session when no x-session-id is sent
A label is a name. It is never a secret and never prompt text. The gateway stores it.
Use letters, digits and ._:- in a session id. The PR stamp accepts only these.
Claude Code sends a session id by itself. You do not need x-session-id for Claude Code.
Examples
curl (every label)
Where: a terminal
bash
# x-client: which tool (max 32 characters)
# x-session-id: one id per run or conversation (max 64)
# x-tags: labels, comma separated
# user (in the body): the person the application acts for
curl https://gateway.example.com/v1/chat/completions \
-H "Authorization: Bearer ${HOOSHYAR_API_KEY}" \
-H "Content-Type: application/json" \
-H "x-client: research-agent" \
-H "x-session-id: run-2026-10-14-0042" \
-H "x-tags: team-data,experiment-7" \
-d '{"model":"auto","user":"alice@example.com","messages":[{"role":"user","content":"ping"}]}'
x-client names the tool, x-session-id the run, x-tags the labels; user in the body names the person. Replace the example values with your own.
OpenAI SDK (Python)
Where: where you build the client
python
import os
from openai import OpenAI
client = OpenAI(
base_url="https://gateway.example.com/v1",
api_key=os.environ["HOOSHYAR_API_KEY"],
default_headers={
"x-client": "research-agent", # which tool, max 32 characters
"x-tags": "team-data,experiment-7", # labels, comma separated
},
)
def ask(messages, session_id: str, user_email: str):
return client.chat.completions.create(
model="auto",
messages=messages,
user=user_email, # the person the application acts for
extra_headers={"x-session-id": session_id}, # one id per run, max 64 characters
)
default_headers carries the labels that do not change. The session id and the user change per run, so they go on each call.
OpenAI SDK (TypeScript)
Where: where you build the client
typescript
import OpenAI from "openai";
const client = new OpenAI({
baseURL: "https://gateway.example.com/v1",
apiKey: process.env.HOOSHYAR_API_KEY,
defaultHeaders: {
"x-client": "research-agent", // which tool, max 32 characters
"x-tags": "team-data,experiment-7", // labels, comma separated
},
});
export function ask(messages: OpenAI.ChatCompletionMessageParam[], sessionId: string, userEmail: string) {
return client.chat.completions.create(
{ model: "auto", messages, user: userEmail }, // the person the application acts for
{ headers: { "x-session-id": sessionId } }, // one id per run, max 64 characters
);
}
defaultHeaders carries the labels that do not change. The session id and the user change per run, so they go on each call.
4. Read the answer
The gateway passes these response headers from the provider to your client, and adds one of its own.
Header
Meaning
x-request-id
The provider's id of the call (request-id on the Anthropic surface). Give it to an administrator when you report a problem.
retry-after
On a 429 or a 503: the number of seconds to wait before you send again.
x-ratelimit-*
The provider's rate-limit counters: x-ratelimit-*, ratelimit-* and anthropic-ratelimit-*.
x-hooshyar-guardrail: soft-deny
The answer is a refusal that the gateway made, shaped like a normal completion, with zero usage. The route is configured to refuse this way instead of a 403.
A blocked request normally gets 403 with error.type = guardrail_blocked. It is the answer. Do not retry it.
Retry only a refused or reset connection, 502, 503 and 504. Send at most 3 tries, with the same x-session-id.
5. Send OpenTelemetry
Use this when your agent calls a model provider directly, not through the gateway. Its usage then appears in the console beside the gateway's own figures. The two are never added together.
The receiver takes the metrics signal only, as OTLP/JSON over HTTP. Protobuf gets 415. Spans and logs get 404, because they can carry prompt text.
Send delta temporality. A cumulative point is rejected, because a sum of cumulative points counts the same tokens many times.
Send your client key in the x-hooshyar-key header. The usage belongs to the owner of that key.
The gateway stores two metrics: gen_ai.client.token.usage and gen_ai.client.operation.duration. Every other metric name is rejected and named in the answer.
An SDK that exports protobuf only (the Python SDK) sends to an OpenTelemetry Collector, and the collector sends JSON to the gateway. The second example shows the collector.
OpenTelemetry export (exporter variables)
Where: the environment of your agent
bash
export OTEL_METRICS_EXPORTER=otlp
export OTEL_EXPORTER_OTLP_PROTOCOL=http/json # JSON only; protobuf is refused
export OTEL_EXPORTER_OTLP_ENDPOINT="https://gateway.example.com/otel" # the exporter adds /v1/metrics
export OTEL_EXPORTER_OTLP_HEADERS="x-hooshyar-key=${HOOSHYAR_API_KEY}" # your client key
export OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE=delta # cumulative points are refused
export OTEL_TRACES_EXPORTER=none # spans are refused
export OTEL_LOGS_EXPORTER=none # logs are refused
export OTEL_RESOURCE_ATTRIBUTES="service.name=research-agent" # which tool, max 32 characters
For an SDK that honours OTEL_EXPORTER_OTLP_PROTOCOL=http/json (the JavaScript SDK, Claude Code). For Claude Code, add CLAUDE_CODE_ENABLE_TELEMETRY=1. OTEL_RESOURCE_ATTRIBUTES sets service.name; set enduser.id and session.id in your code, per run.
OpenTelemetry Collector (for an SDK that exports protobuf only)
Where: otel-collector-config.yaml, next to your agent
The Python SDK exports protobuf only. Point it at the collector (port 4318), keep OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE=delta in the agent, and the collector sends JSON to the gateway. Only a metrics pipeline is defined, so spans and logs are not forwarded.
The attributes
Put these on the resource or on each data point.
Attribute
Meaning
Example
service.name
The name of your tool. The console shows it as the client. Max 32 characters.
research-agent
gen_ai.request.model
The model the operation asked for.
<MODEL_ID>
gen_ai.token.type
input or output, on each token-usage point.
input
enduser.id
The user the agent acts for (user.id is accepted too). Recorded beside the key owner, never instead of the owner.
alice@example.com
session.id
One id per run or conversation. The console counts sessions from it.
run-42
What else is stored
These labels, when present: gen_ai.system, gen_ai.provider.name, gen_ai.operation.name, gen_ai.response.model, gen_ai.agent.name, server.address, server.port, error.type, telemetry.sdk.name, telemetry.sdk.language, telemetry.sdk.version. Every other attribute is dropped and its name is recorded. A value longer than 128 characters is dropped whole. Prompt text and completion text are never stored.
The answers
Status
Meaning
What to do
200
Stored. partialSuccess.rejectedDataPoints counts the points that were refused, and errorMessage says why.
Read errorMessage in your exporter log. Do not send the refused points again.
400
The body is not an OTLP metrics export.
Fix the exporter. Do not retry.
401
No valid client key in x-hooshyar-key.
Fix the key. Do not retry.
404
The receiver is off, or you sent the logs or the traces signal.
Ask an administrator, or set the traces and logs exporters to none.
413
The export is larger than the gateway's JSON limit.
Export more often, with fewer points.
415
The body is protobuf.
Set OTEL_EXPORTER_OTLP_PROTOCOL=http/json, or send through a collector.
502
The gateway could not verify the key.
Retry later.
503
The gateway could not store the export.
Retry later. The exporter does this by itself.
Claude Code can send its own metrics (claude_code.*) the same way. Set the same variables and CLAUDE_CODE_ENABLE_TELEMETRY=1. An administrator may have installed this already in a managed settings file.
6. Stamp a pull request
Use this when your agent opens pull requests. The reports then show what one merged PR cost. An administrator turns the feature on first.
Send one x-session-id per task on every request. Write the same id as one line in the PR body: Hooshyar-Session: <the session id>. Write the same line in a review body to count the review.
A PR with no line, or with two different ids, counts as a merge with no cost. The gateway never guesses a session from the time.
Not for your agent
/otel/v1/logs and /otel/v1/traces: refused, because they carry prompt text.
/hooks/v1/events: receives the decisions of the managed Claude Code hooks that an administrator installs. An application does not call it.
Check it
Send one request with your labels. In the console, open Reporting and select the Client or the Session view. An OpenTelemetry export appears under Reported by the agent after the exporter's first interval.