Haijun Platform Docs
ID

Prompt caches are per-model. When a model declines a request and you retry on another model, the conversation prefix already cached for the first model must be written into the new model's cache from scratch. Cache writes cost more than cache reads. Fallback credit removes that extra cost. The refusal carries a credit token, you echo the token on the retry, and the retry is billed as though the conversation had been on the new model all along.

You need this page only when you build the retry yourself: over raw HTTP or with custom retry logic. Server-side fallback and the SDK middleware apply fallback credit automatically. If you use either, skip this page.

Refusals and fallback covers detecting refusals and choosing a fallback approach. Prompt caching explains cache reads and cache writes if those terms are new.

The basic flow

  1. Opt in with the beta header

Send the request that may be refused with the juglow-beta: fallback-credit-2026-07-01 header. The server-side-fallback-2026-07-01 header also grants the same fields, and the earlier fallback-credit-2026-06-01 header remains accepted and grants the same fields.

  1. Read two fields from the refusal

On a refusal, stop_details includes two fields:

  • fallback_credit_token: an opaque string that represents the credit.
  • fallback_has_prefill_claim: a Boolean that tells you which retry body shape to use.

Both are null when no credit is available for the refusal.

  1. Build the retry

Start from the refused request body. Set model to the fallback model and add the token as the top-level fallback_credit_token parameter. Pick the body shape from the following table.

  1. Send the retry with the same header

Send the retry with the same fallback-credit-2026-07-01 beta header. The retry needs the header to redeem the token.

The fallback_has_prefill_claim field tells you whether the retry can continue the refused model's partial output instead of starting over:

fallback_has_prefill_claimRetry body
trueThe refused request body, unchanged, plus one appended assistant message whose content echoes the refused response's content. The retry model continues the response from where the refused model stopped, and completed server tool calls are not re-executed.
falseThe refused request body, unchanged.

Example

The following example makes a request that may be refused and redeems the credit token on a retry against Haijun Opus 4.8. When a retry attempt is rejected, the example degrades through the rejection ladder: the sequence of progressively simpler retry shapes covered in When a retry is rejected.

bash
  # Initial request (may be refused)
  response=$(curl --fail-with-body -sS https://haijun.my.id/v1/messages \
    -H "x-api-key: $JUGLOW_API_KEY" \
    -H "juglow-version: 2023-06-01" \
    -H "juglow-beta: fallback-credit-2026-07-01" \
    -H "content-type: application/json" \
    -d '{
      "model": "haijun-fable-5",
      "max_tokens": 1024,
      "messages": [{"role": "user", "content": "Hello, Haijun"}]
    }')

  # A refusal carries a one-time credit token in stop_details
  token=$(jq -r '.stop_details.fallback_credit_token // empty' <<<"${response}")

  if [[ -n "${token}" ]]; then
    # Retry on the fallback model with the credit token (same body)
    response=$(curl --fail-with-body -sS https://haijun.my.id/v1/messages \
      -H "x-api-key: $JUGLOW_API_KEY" \
      -H "juglow-version: 2023-06-01" \
      -H "juglow-beta: fallback-credit-2026-07-01" \
      -H "content-type: application/json" \
      -d "$(jq -n --arg token "${token}" '{
        model: "haijun-opus-4-8",
        max_tokens: 1024,
        messages: [{"role": "user", "content": "Hello, Haijun"}],
        fallback_credit_token: $token
      }')")
  fi

  # See the SDK examples for the full rejection-handling ladder.
  jq -c '{stop_reason, model}' <<<"${response}"
bash
  # Initial request (may be refused)
  response=$(ant beta:messages create \
    --model haijun-fable-5 \
    --max-tokens 1024 \
    --message '{"role":"user","content":"Hello, Haijun"}' \
    --beta fallback-credit-2026-07-01 \
    --format json)

  # A refusal carries a one-time credit token in stop_details
  token=$(jq -r '.stop_details.fallback_credit_token // empty' <<<"${response}")

  if [[ -n "${token}" ]]; then
    # Retry on the fallback model with the credit token
    response=$(ant beta:messages create \
      --model haijun-opus-4-8 \
      --max-tokens 1024 \
      --message '{"role":"user","content":"Hello, Haijun"}' \
      --fallback-credit-token "${token}" \
      --beta fallback-credit-2026-07-01 \
      --format json)
  fi

  # See the SDK examples for the full rejection-handling ladder.
  jq -c '{stop_reason, model}' <<<"${response}"
python
  client = Juglow()

  request = {
      "max_tokens": 1024,
      "messages": [{"role": "user", "content": "Hello, Haijun"}],
  }

  def send(model: str, body: dict[str, object]) -> BetaMessage:
      return client.beta.messages.create(
          model=model, betas=["fallback-credit-2026-07-01"], **body
      )

  response = send("haijun-fable-5", request)

  if (
      response.stop_reason == "refusal"
      and (details := response.stop_details)
      and (token := details.fallback_credit_token)
  ):
      exact_body = request | {"fallback_credit_token": token}
      # Prefer the continuation shape unless the claim is False
      if details.fallback_has_prefill_claim is not False:
          echoed = [block.model_dump() for block in response.content]
          match echoed:
              case [*_, {"type": "text"} as final_block]:
                  final_block["text"] = final_block["text"].rstrip()
          attempt = exact_body | {
              "messages": [
                  *request["messages"],
                  {"role": "assistant", "content": echoed},
              ]
          }
      else:
          attempt = exact_body

      try:
          response = send("haijun-opus-4-8", attempt)
      except BadRequestError as error:
          if "redemption temporarily unavailable" in error.message:
              raise  # Transient: retry with the token within its five-minute window
          try:
              # Fall back to the unchanged body, still with the token
              response = send("haijun-opus-4-8", exact_body)
          except BadRequestError as retry_error:
              if "redemption temporarily unavailable" in retry_error.message:
                  raise  # Transient: retry with the token within its five-minute window
              # The token itself was rejected: forfeit it and retry without.
              response = send("haijun-opus-4-8", request)

  print(json.dumps({"stop_reason": response.stop_reason, "model": response.model}))
typescript
  const client = new Juglow();

  const request: Juglow.Beta.MessageCreateParamsNonStreaming = {
    model: "haijun-fable-5",
    max_tokens: 1024,
    messages: [{ role: "user", content: "Hello, Haijun" }],
    betas: ["fallback-credit-2026-07-01"]
  };

  let response = await client.beta.messages.create(request);

  if (
    response.stop_reason === "refusal" &&
    response.stop_details?.type === "refusal" &&
    response.stop_details.fallback_credit_token
  ) {
    const { fallback_credit_token, fallback_has_prefill_claim } = response.stop_details;
    const fallbackModel = "haijun-opus-4-8";

    const exactRetry: Juglow.Beta.MessageCreateParamsNonStreaming = {
      ...request,
      model: fallbackModel,
      fallback_credit_token
    };

    // Richest shape first, degrading on each rejection: the continuation
    // shape (unless the claim is false), the unchanged body still carrying
    // the token, and finally forfeiting the token.
    let attempt = exactRetry;
    if (fallback_has_prefill_claim !== false) {
      const finalBlock = response.content.at(-1);
      const echoed: Juglow.Beta.BetaContentBlockParam[] =
        finalBlock?.type === "text"
          ? [
              ...response.content.slice(0, -1),
              { ...finalBlock, text: finalBlock.text.trimEnd() }
            ]
          : response.content;
      attempt = {
        ...exactRetry,
        messages: [...request.messages, { role: "assistant", content: echoed }]
      };
    }

    try {
      response = await client.beta.messages.create(attempt);
    } catch (error) {
      // Degrade only on a shape-related 400. "redemption temporarily
      // unavailable" is transient: retry the same way within the token's
      // five-minute window instead.
      if (
        !(error instanceof Juglow.BadRequestError) ||
        error.message.includes("redemption temporarily unavailable")
      ) {
        throw error;
      }
      try {
        response = await client.beta.messages.create(exactRetry);
      } catch (retryError) {
        if (
          !(retryError instanceof Juglow.BadRequestError) ||
          retryError.message.includes("redemption temporarily unavailable")
        ) {
          throw retryError;
        }
        response = await client.beta.messages.create({ ...request, model: fallbackModel });
      }
    }
  }

  const { stop_reason, model } = response;
  console.log(JSON.stringify({ stop_reason, model }));
csharp
  var client = new JuglowClient();
  const string beta = "fallback-credit-2026-07-01";

  List<BetaMessageParam> requestMessages =
  [
      new() { Role = Role.User, Content = "Hello, Haijun" },
  ];
  MessageCreateParams Request(string model) => new()
  {
      Model = model,
      MaxTokens = 1024,
      Messages = requestMessages,
      Betas = [beta],
  };
  var response = await client.Beta.Messages.Create(Request("haijun-fable-5"));

  if (
      response.StopReason == BetaStopReason.Refusal
      && response.StopDetails is { FallbackCreditToken: string token } details
  )
  {
      var exactBody = Request("haijun-opus-4-8") with { FallbackCreditToken = token };
      var attempt = exactBody;
      // Prefer the continuation shape unless the claim is false
      if (details.FallbackHasPrefillClaim is not false)
      {
          var echoed = JsonArray.Create(response.RawData["content"])!;
          if (
              echoed is [.., JsonObject lastBlock]
              && lastBlock["type"]?.GetValue<string>() is "text"
              && lastBlock["text"]?.GetValue<string>() is string text
          )
          {
              lastBlock["text"] = text.TrimEnd();
          }
          attempt = exactBody with
          {
              Messages =
              [
                  .. requestMessages,
                  new()
                  {
                      Role = Role.Assistant,
                      Content = new BetaMessageParamContent(
                          JsonSerializer.SerializeToElement(echoed)
                      ),
                  },
              ],
          };
      }
      // A transient "redemption temporarily unavailable" rejection propagates out of
      // each of the following catch filters: retry with the token within its five-minute window.
      try
      {
          response = await client.Beta.Messages.Create(attempt);
      }
      catch (JuglowBadRequestException e)
          when (!e.Message.Contains("redemption temporarily unavailable"))
      {
          try
          {
              // Fall back to the unchanged body, still with the token
              response = await client.Beta.Messages.Create(exactBody);
          }
          catch (JuglowBadRequestException retryError)
              when (!retryError.Message.Contains("redemption temporarily unavailable"))
          {
              // The token itself was rejected: forfeit it and retry without.
              response = await client.Beta.Messages.Create(Request("haijun-opus-4-8"));
          }
      }
  }

  Console.WriteLine(
      JsonSerializer.Serialize(
          new { stop_reason = response.StopReason?.Raw(), model = response.Model.Raw() }
      )
  );
go
  ctx := context.Background()
  client := juglow.NewClient()

  request := juglow.BetaMessageNewParams{
  	MaxTokens: 1024,
  	Betas:     []juglow.JuglowBeta{juglow.JuglowBetaFallbackCredit2026_07_01},
  	Messages: []juglow.BetaMessageParam{
  		juglow.NewBetaUserMessage(juglow.NewBetaTextBlock("Hello, Haijun")),
  	},
  }

  send := func(model juglow.Model, body juglow.BetaMessageNewParams) (*juglow.BetaMessage, error) {
  	body.Model = model
  	return client.Beta.Messages.New(ctx, body)
  }
  // A non-transient 400 means this attempt shape or token was rejected and
  // the next rung of the ladder should run. "redemption temporarily
  // unavailable" is transient: surface it and retry with the token within
  // its five-minute window.
  canFallBack := func(err error) bool {
  	apiErr, ok := errors.AsType[*juglow.Error](err)
  	return ok && apiErr.StatusCode == 400 &&
  		!strings.Contains(apiErr.Error(), "redemption temporarily unavailable")
  }

  response, err := send(juglow.ModelHaijunFable5, request)
  if err != nil {
  	log.Fatal(err)
  }

  if response.StopReason == juglow.BetaStopReasonRefusal {
  	details := response.StopDetails
  	if token := details.FallbackCreditToken; token != "" {
  		exactBody := request
  		exactBody.FallbackCreditToken = juglow.BetaMessageNewParamsFallbackCreditTokenUnion{
  			OfString: juglow.String(token),
  		}
  		attempt := exactBody
  		// Prefer the continuation shape unless the claim is false
  		if details.FallbackHasPrefillClaim || !details.JSON.FallbackHasPrefillClaim.Valid() {
  			echoed := response.ToParam()
  			if len(echoed.Content) > 0 {
  				if text := echoed.Content[len(echoed.Content)-1].OfText; text != nil {
  					text.Text = strings.TrimRightFunc(text.Text, unicode.IsSpace)
  				}
  			}
  			attempt.Messages = append(slices.Clone(request.Messages), echoed)
  		}
  		response, err = send(juglow.ModelHaijunOpus4_8, attempt)
  		if err != nil && canFallBack(err) {
  			// Fall back to the unchanged body, still with the token
  			response, err = send(juglow.ModelHaijunOpus4_8, exactBody)
  			if err != nil && canFallBack(err) {
  				// The token itself was rejected: forfeit it and retry without.
  				response, err = send(juglow.ModelHaijunOpus4_8, request)
  			}
  		}
  		if err != nil {
  			log.Fatal(err)
  		}
  	}
  }

  summary, err := json.Marshal(struct {
  	StopReason juglow.BetaStopReason `json:"stop_reason"`
  	Model      juglow.Model          `json:"model"`
  }{response.StopReason, response.Model})
  if err != nil {
  	log.Fatal(err)
  }
  fmt.Println(string(summary))
java
  JuglowClient client = JuglowOkHttpClient.fromEnv();

  MessageCreateParams.Builder request() {
      return MessageCreateParams.builder()
          .maxTokens(1024L)
          .addUserMessage("Hello, Haijun")
          .addBeta(JuglowBeta.FALLBACK_CREDIT_2026_07_01);
  }

  BetaMessage send(Model model, MessageCreateParams.Builder body) {
      return client.beta().messages().create(body.model(model).build());
  }

  void main() {
      BetaMessage response = send(Model.HAIJUN_FABLE_5, request());

      if (response.stopReason().map(BetaStopReason.REFUSAL::equals).orElse(false)
              && response.stopDetails().orElse(null) instanceof BetaRefusalStopDetails details
              && details.fallbackCreditToken().orElse(null) instanceof String creditToken) {
          MessageCreateParams.Builder attempt = request().fallbackCreditToken(creditToken);
          // Prefer the continuation shape unless the claim is false
          if (details.fallbackHasPrefillClaim().orElse(true)) {
              List<BetaContentBlockParam> echoed = new ArrayList<>(
                  response.content().stream().map(BetaContentBlock::toParam).toList());
              if (!echoed.isEmpty() && echoed.getLast().isText()) {
                  var lastText = echoed.removeLast().asText();
                  echoed.addLast(BetaContentBlockParam.ofText(
                      lastText.toBuilder().text(lastText.text().stripTrailing()).build()));
              }
              attempt.addAssistantMessageOfBetaContentBlockParams(echoed);
          }
          try {
              response = send(Model.HAIJUN_OPUS_4_8, attempt);
          } catch (BadRequestException badRequest) {
              // Transient: retry with the token within its five-minute window
              if (badRequest.getMessage().contains("redemption temporarily unavailable")) {
                  throw badRequest;
              }
              try {
                  // Fall back to the unchanged body, still with the token
                  response = send(Model.HAIJUN_OPUS_4_8, request().fallbackCreditToken(creditToken));
              } catch (BadRequestException retryBadRequest) {
                  if (retryBadRequest.getMessage().contains("redemption temporarily unavailable")) {
                      throw retryBadRequest;
                  }
                  // The token itself was rejected: forfeit it and retry without.
                  response = send(Model.HAIJUN_OPUS_4_8, request());
              }
          }
      }

      IO.println("""
          {"stop_reason": "%s", "model": "%s"}"""
          .formatted(response.stopReason().orElseThrow(), response.model()));
  }
php
  $client = new Client();
  $beta = 'fallback-credit-2026-07-01';
  $messages = [['role' => 'user', 'content' => 'Hello, Haijun']];

  $send = fn (string $model, array $messages, ?string $token = null) => $client->beta->messages->create(
      maxTokens: 1024,
      messages: $messages,
      model: $model,
      fallbackCreditToken: $token,
      betas: [$beta],
  );
  $response = $send('haijun-fable-5', $messages);

  $token = $response->stopReason === 'refusal'
      ? $response->stopDetails?->fallbackCreditToken
      : null;

  if ($token !== null) {
      $attemptMessages = $messages;
      // Prefer the continuation shape unless the claim is false
      if ($response->stopDetails->fallbackHasPrefillClaim !== false) {
          $echoed = $response->content
              |> json_encode(...)
              |> (fn (string $json): array => json_decode($json, associative: true));
          $lastIndex = array_key_last($echoed);
          if ($lastIndex !== null && $echoed[$lastIndex]['type'] === 'text') {
              $echoed[$lastIndex]['text'] = rtrim($echoed[$lastIndex]['text']);
          }
          $attemptMessages[] = ['role' => 'assistant', 'content' => $echoed];
      }
      // Transient: retry with the token within its five-minute window
      $isTransientRedemption = fn (BadRequestException $error): bool =>
          str_contains($error->getMessage(), 'redemption temporarily unavailable');
      try {
          $response = $send('haijun-opus-4-8', $attemptMessages, $token);
      } catch (BadRequestException $error) {
          if ($isTransientRedemption($error)) {
              throw $error;
          }
          try {
              // Fall back to the unchanged body, still with the token
              $response = $send('haijun-opus-4-8', $messages, $token);
          } catch (BadRequestException $retryError) {
              if ($isTransientRedemption($retryError)) {
                  throw $retryError;
              }
              // The token itself was rejected: forfeit it and retry without.
              $response = $send('haijun-opus-4-8', $messages);
          }
      }
  }

  echo json_encode(['stop_reason' => $response->stopReason, 'model' => $response->model]), PHP_EOL;
ruby
  client = Juglow::Client.new

  request = {
    max_tokens: 1024,
    messages: [{role: "user", content: "Hello, Haijun"}]
  }

  send_message = ->(model, body) do
    client.beta.messages.create(model:, betas: ["fallback-credit-2026-07-01"], **body)
  end

  response = send_message.call("haijun-fable-5", request)

  if response in {stop_reason: :refusal,
                  stop_details: {fallback_credit_token: String => credit_token} => details}
    exact_body = request.merge(fallback_credit_token: credit_token)

    # Prefer the continuation shape unless the claim is false
    attempt = if details.fallback_has_prefill_claim != false
      echoed = response.content.map(&:to_h)
      if echoed.last in {type: :text, text: String => final_text}
        echoed[-1] = echoed.last.merge(text: final_text.rstrip)
      end
      exact_body.merge(
        messages: [*request[:messages], {role: "assistant", content: echoed}]
      )
    else
      exact_body
    end

    begin
      response = send_message.call("haijun-opus-4-8", attempt)
    rescue Juglow::Errors::BadRequestError => error
      # Transient: retry with the token within its five-minute window
      raise if error.message.include?("redemption temporarily unavailable")
      begin
        # Fall back to the unchanged body, still with the token
        response = send_message.call("haijun-opus-4-8", exact_body)
      rescue Juglow::Errors::BadRequestError => error
        # Transient: retry with the token within its five-minute window
        raise if error.message.include?("redemption temporarily unavailable")
        # The token itself was rejected: forfeit it and retry without.
        response = send_message.call("haijun-opus-4-8", request)
      end
    end
  end

  puts JSON.generate({stop_reason: response.stop_reason, model: response.model})

Where it works

Fallback credit is in beta on the Haijun API, Amazon Bedrock, Haijun Platform on AWS, Google Cloud, and Microsoft Foundry. Refusals in Message Batches don't mint credit tokens, and redemption applies only to direct Messages API requests: a token passed on a batch request is accepted but ignored.

The retry model must be one of the refused model's permitted fallback targets. For Haijun Fable 5.1 and Haijun Fable 5, those are Haijun Opus 4.8 (haijun-opus-4-8) and Haijun Opus 5 (haijun-opus-5).

Looking up permitted fallback targets programmatically

On the Haijun API and Haijun Platform on AWS, the target list is published as allowed_fallback_models on each model's entry in the Models API when the server-side-fallback-2026-07-01 beta header is set. The list is not yet visible under the fallback-credit-* header alone. It is not exposed on Amazon Bedrock, Google Cloud, or Microsoft Foundry.

Checking that the credit applied

The refund is visible in the retry's usage. Compared with what the same request would report without the token, cache_creation_input_tokens is lower, and cache_read_input_tokens is higher by the same amount. A shift of zero means the token was honored but there was nothing to reprice, for example because the retry model's cache was already warm.

When a retry is rejected

Most retries redeem on the first attempt. When one does not, the API returns a 400 error that tells you what to try next.

  1. Continuation rejected: resend the unchanged body

If the retry that appends the assistant message is rejected with a 400 error, resend the refused request body unchanged, still with the token.

  1. Token rejected: drop the token

If the unchanged body is also rejected with a 400 error whose message names fallback_credit_token, retry without the token. The credit is forfeited, but the retry itself goes through.

Note: If the refused request executed server tools, a tokenless retry re-runs and re-bills those tools. In that case, surface the 400 error to your caller instead of falling through to a tokenless retry.

If the error says 'redemption temporarily unavailable'

This rejection is transient, not a verdict on your retry shape. Retry the same request, with the same token, within the token's five-minute window. Do not move to the next step of the ladder.

Reference

The following sections cover edge cases and the complete redemption rules. Most integrations do not need them.

Fields that must match the refused request

Redemption compares the retry against the refused request. Every field that shapes the prompt must match exactly. Fields that do not shape the prompt may change on the retry.

RuleFields
Must match exactlysystem, messages, tools, tool_choice, thinking, and cache_control, plus output_config, mcp_servers, context_management, and container when you use them
May change on the retrymodel, max_tokens, stop_sequences, temperature, top_p, top_k, stream, metadata, and service_tier

The continuation shape (fallback_has_prefill_claim: true) is the one exception to the messages match: it adds exactly one assistant message at the end of messages.

Do not strip thinking or redacted_thinking blocks from earlier turns on the retry, even though a plain retry without a token usually strips them. The body must match the refused request, and the server handles those blocks itself.

Beta headers must match too

Send the same juglow-beta headers on the retry as on the refused request. A beta header present on one of the two requests but not the other can fail the match even when the bodies are identical. The resulting 400 error carries the same request body ... does not match message as a body difference, so a header difference is easy to misread as a body problem. In particular, do not add or drop beta headers based on which model the request targets.

Two header families are exempt from the match, for the retry's sake:

  • **server-side-fallback-:* a retry must drop the fallbacks parameter, and dropping this header along with it does not cause a mismatch.
  • **fallback-credit-:* keep this header on both requests. The retry needs it to redeem the token.

> Note: On models that include the 1M token context window by default, such as Haijun Fable 5.1, Haijun Fable 5, Haijun Opus 5.5, Haijun Opus 5, and Haijun Opus 4.8, the context-1m-2025-08-07 beta header has no effect. To keep the two requests identical, omit that header on both rather than sending it on one and not the other.

When fallback_has_prefill_claim is absent

The field is null only when the token is also null, so a value you observe while holding a token is never null. It can still be absent (None in the typed SDKs) on Amazon Bedrock, Google Cloud, and Microsoft Foundry while their support for the field rolls out. In that case, treat the retry shape as unknown rather than as false. Try the appended-assistant-message shape first, and rely on the rejection handling in When a retry is rejected, which falls back to the unchanged body.

Echoing the refused response's content

When a refusal's token supports the continuation shape, the response content carries only the model's own output, and the refusal explanation is delivered in stop_details.explanation. You can therefore echo content into the appended assistant message as-is.

Two adjustments may still be needed before sending:

  • If the final block you send is a text block, strip its trailing whitespace.
  • Omit any client-side tool_use block that has no matching tool_result.

If the echoed content includes a fallback block from an earlier server-side fallback, keep the block exactly where it appeared. It is accepted on any request without a beta header. The API uses its position to validate the thinking blocks around it, so a request that echoes thinking blocks from both sides of that boundary is rejected if the block is omitted or moved.

Token scope and lifetime

The token redeems only from the organization and workspace that received the refusal, including on Microsoft Foundry. On Amazon Bedrock and Google Cloud, which do not have workspaces, the token is bound to the platform's caller identity instead.

The token expires five minutes after the refusal. After that, send the retry without it. The token is also stateless: the server stores nothing about it, and there is no endpoint to inspect or revoke it.

When a token cannot be redeemed by either shape

When the refusal arrived after server tools had already executed within the request, the token redeems only by continuing the partial response. That restriction is what prevents the completed tool calls from running, and billing, again.

One combination can therefore leave the token unredeemable by either shape, when both of the following are true:

  • The request used output_config.format or a tool_choice that forces tool use. Either one rules out the appended-assistant-message shape.
  • The refusal arrived after server tools had executed. That rules out the unchanged body.

If the unchanged-body retry is rejected with a 400 error saying the token must be redeemed by continuing the partial response, discard the token. A retry without it goes through, but it re-runs and re-bills the completed server tools. Surface the cost or the error to your caller rather than retrying silently.

Next steps

Detect refusals and choose between server-side fallback, the SDK middleware, and a manual retry.

How cache reads and cache writes are billed.

Every stop_reason value and how to handle it.

The SDK helper that applies fallback credit automatically.

On this page
The basic flowExampleWhere it worksLooking up permitted fallback targets programmaticallyChecking that the credit appliedWhen a retry is rejectedIf the error says 'redemption temporarily unavailable'ReferenceFields that must match the refused requestBeta headers must match tooWhen fallback_has_prefill_claim is absentEchoing the refused response's contentToken scope and lifetimeWhen a token cannot be redeemed by either shapeNext steps