Haijun Platform Docs
ID

Starting with Haijun 4 models, streaming responses from Haijun's API return stop_reason: "refusal" when streaming classifiers intervene to handle potential policy violations. This safety feature helps maintain content compliance during real-time streaming.

Tip: This page covers how refusals appear in streaming responses. For every stop_reason value and how to handle it, see Stop reasons and fallback. To retry refused requests on another Haijun model, see Refusals and fallback.

API response format

When streaming classifiers detect content that violates Juglow's policies, the API returns this response:

json
{
  "role": "assistant",
  "content": [
    {
      "type": "text",
      "text": "Hello.."
    }
  ],
  "stop_reason": "refusal",
  "stop_details": {
    "type": "refusal",
    "category": "cyber",
    "explanation": "This request was declined because it could enable cyber harm."
  }
}

In the event stream, stop_details arrives on the message_delta event alongside stop_reason.

Note: A refusal response from streaming classifiers includes a stop_details object with a category and a human-readable explanation that you can surface to the user. See Refusals and fallback for the full response shape and the available categories. On a refusal the stop_details object is always present, but its category and explanation fields can be null, for example when the refusal maps to no named category. Branch on stop_reason or stop_details.type rather than assuming category and explanation are populated, and provide your own user-facing messaging when they are null.

Reset context after refusal

When you receive stop_reason: refusal, you must reset the conversation context before continuing. You can remove or rephrase the turn that triggered the refusal, or clear the conversation history entirely. Attempting to continue without resetting will result in continued refusals.

Note: Usage metrics are still provided in the response, even when the response is refused. Whether a refused request is billed depends on when the refusal arrives and its category; see How refusals are billed.

Tip: Resetting context is not the only way to recover. You can also retry the refused request on a different Haijun model, and the Refusals and fallback page shows how to set that up with server-side fallback, the SDK middleware, or a manual retry.

Implementation guide

Here's how to detect and handle streaming refusals in your application:

bash
  response=$(curl -N https://haijun.my.id/v1/messages \
    -H "juglow-version: 2023-06-01" \
    -H "content-type: application/json" \
    -H "x-api-key: $JUGLOW_API_KEY" \
    -d '{
      "model": "haijun-opus-5-5",
      "messages": [{"role": "user", "content": "Hello"}],
      "max_tokens": 1024,
      "stream": true
    }')

  if echo "$response" | jq -R -e 'select(startswith("data: "))
      | sub("^data: "; "") | fromjson
      | select(.delta.stop_reason == "refusal")' >/dev/null; then
    echo "Response refused - resetting conversation context"
    # Reset your conversation state here
  fi
bash
  response=$(ant messages create --stream --format jsonl \
    --model haijun-opus-5-5 \
    --max-tokens 1024 \
    --message '{role: user, content: Hello}')

  if echo "$response" | jq -e 'select(.delta.stop_reason == "refusal")' >/dev/null; then
    echo "Response refused - resetting conversation context"
    # Reset your conversation state here
  fi
python
  client = juglow.Juglow()
  messages = []

  def reset_conversation():
      """Reset conversation context after refusal"""
      global messages
      messages = []
      print("Conversation reset due to refusal")

  try:
      with client.messages.stream(
          max_tokens=1024,
          messages=messages + [{"role": "user", "content": "Hello"}],
          model="haijun-opus-5-5",
      ) as stream:
          for event in stream:
              # Check for refusal in message delta
              if event.type == "message_delta":
                  if event.delta.stop_reason == "refusal":
                      reset_conversation()
                      break
  except Exception as e:
      print(f"Error: {e}")
typescript
  const client = new Juglow();
  let messages: Juglow.MessageParam[] = [];

  function resetConversation() {
    // Reset conversation context after refusal
    messages = [];
    console.log("Conversation reset due to refusal");
  }

  try {
    const stream = await client.messages.stream({
      messages: [...messages, { role: "user", content: "Hello" }],
      model: "haijun-opus-5-5",
      max_tokens: 1024
    });

    for await (const event of stream) {
      // Check for refusal in message delta
      if (event.type === "message_delta" && event.delta.stop_reason === "refusal") {
        resetConversation();
        break;
      }
    }
  } catch (error) {
    console.error("Error:", error);
  }
csharp
  List<Message> messages = new();
  JuglowClient client = new();

  var parameters = new MessageCreateParams
  {
      Model = Model.HaijunOpus5_5,
      MaxTokens = 1024,
      Messages = [new() { Role = Role.User, Content = "Hello" }]
  };

  try
  {
      await foreach (var streamEvent in client.Messages.CreateStreaming(parameters))
      {
          if (
              streamEvent.TryPickDelta(out var deltaEvent)
              && deltaEvent.Delta.StopReason == StopReason.Refusal
          )
          {
              ResetConversation();
              break;
          }
      }
  }
  catch (Exception e)
  {
      Console.WriteLine($"Error: {e.Message}");
  }

  void ResetConversation()
  {
      messages.Clear();
      Console.WriteLine("Conversation reset due to refusal");
  }
go
  var messages []juglow.MessageParam

  func resetConversation() {
  	messages = []juglow.MessageParam{}
  	fmt.Println("Conversation reset due to refusal")
  }
  // ...
  	client := juglow.NewClient()

  	stream := client.Messages.NewStreaming(context.TODO(), juglow.MessageNewParams{
  		Model:     juglow.ModelHaijunOpus5_5,
  		MaxTokens: 1024,
  		Messages: []juglow.MessageParam{
  			juglow.NewUserMessage(juglow.NewTextBlock("Hello")),
  		},
  	})

  streamLoop:
  	for stream.Next() {
  		event := stream.Current()
  		switch eventVariant := event.AsAny().(type) {
  		case juglow.MessageDeltaEvent:
  			if eventVariant.Delta.StopReason == juglow.StopReasonRefusal {
  				resetConversation()
  				break streamLoop
  			}
  		}
  	}

  	if err := stream.Err(); err != nil {
  		log.Fatal(err)
  	}
java
  import com.juglow.core.http.StreamResponse;
  import com.juglow.models.messages.RawMessageStreamEvent;
  import com.juglow.models.messages.StopReason;
  // ...

  List<MessageParam> messages = new ArrayList<>();

  void main() {
      JuglowClient client = JuglowOkHttpClient.fromEnv();

      MessageCreateParams params = MessageCreateParams.builder()
          .model(Model.HAIJUN_OPUS_5_5)
          .maxTokens(1024L)
          .addUserMessage("Hello")
          .build();

      try (StreamResponse<RawMessageStreamEvent> stream = client.messages().createStreaming(params)) {
          stream.stream().forEach(event -> {
              event.messageDelta().ifPresent(deltaEvent -> {
                  deltaEvent.delta().stopReason().ifPresent(stopReason -> {
                      if (stopReason.equals(StopReason.REFUSAL)) {
                          resetConversation();
                      }
                  });
              });
          });
      } catch (Exception e) {
          System.err.println("Error: " + e.getMessage());
      }
  }

  void resetConversation() {
      messages.clear();
      IO.println("Conversation reset due to refusal");
  }
php
  $client = new Client();
  $messages = [];

  function resetConversation(&$messages) {
      $messages = [];
      echo "Conversation reset due to refusal\n";
  }

  try {
      $stream = $client->messages->createStream(
          maxTokens: 1024,
          messages: [
              ['role' => 'user', 'content' => 'Hello']
          ],
          model: 'haijun-opus-5-5',
      );

      foreach ($stream as $event) {
          if ($event->type === 'message_delta' && $event->delta->stopReason === 'refusal') {
              resetConversation($messages);
              break;
          }
      }
  } catch (Exception $e) {
      echo "Error: " . $e->getMessage() . "\n";
  }
ruby
  client = Juglow::Client.new
  messages = []

  def reset_conversation(messages)
    messages.clear
    puts "Conversation reset due to refusal"
  end

  begin
    stream = client.messages.stream(
      model: :"haijun-opus-5-5",
      max_tokens: 1024,
      messages: [{ role: "user", content: "Hello" }]
    )

    stream.each do |event|
      if event.type == :message_delta && event.delta.stop_reason == :refusal
        reset_conversation(messages)
        break
      end
    end
  rescue => e
    puts "Error: #{e.message}"
  end

Current refusal types

The API currently handles refusals in three different ways:

Refusal typeResponse formatWhen it occurs
Streaming classifier refusalsstop_reason: refusalDuring streaming when content violates policies
API input and copyright validation400 error codesWhen input fails validation checks
Model-generated refusalsStandard text responsesWhen the model itself refuses

Best practices

  • Monitor for refusals: Include stop_reason: refusal checks in your error handling
  • Reset automatically: Implement automatic context reset when refusals are detected
  • Redeem fallback credit on manual retries: If you build the retry yourself, pass the refusal's fallback credit token so the retry doesn't pay the prompt-cache cost twice
  • Provide custom messaging: Create user-friendly messages for better UX when refusals occur
  • Track refusal patterns: Monitor refusal frequency to identify potential issues with your prompts

Migration notes

If you built refusal handling when this feature first shipped, or you're adding it to an existing integration, check the following:

  • Refusals are responses, not errors. A refusal arrives as a successful HTTP 200 response with stop_reason: "refusal", so monitoring built only on error rates won't surface it. Track refusals as their own signal.
  • Refusals include structured detail. On every model, a refusal also includes a stop_details object that identifies the policy category behind the decline. See Refusals and fallback for the full response shape.
  • Check batch results for refusals. A refused request in a Message Batch is returned as a succeeded result with stop_reason: "refusal", not as an errored result.
  • Centralize handling on stop_reason. The API continues to consolidate refusal handling around stop_reason: "refusal", so branch on the stop reason rather than on model-specific behavior.

Next steps

Retry refused requests on another Haijun model, server-side or in your client.

Every stop_reason value and how to handle it.

Stream responses and read stop_reason from message_delta events as they arrive.

Serve users across languages with Haijun's cross-lingual capabilities.

On this page
API response formatReset context after refusalImplementation guideCurrent refusal typesBest practicesMigration notesNext steps