백그라운드 모드

백그라운드 모드 (Background mode)

Codex나 Deep Research 같은 에이전트들은 추론 모델이 복잡한 문제를 푸는 데 몇 분씩 걸릴 수 있다는 걸 보여줘요. 백그라운드 모드를 쓰면 GPT-5.2, GPT-5.2 Pro 같은 모델에서 오래 걸리는 작업을 타임아웃이나 연결 문제 걱정 없이 안정적으로 실행할 수 있어요.

백그라운드 모드는 이런 작업을 비동기로 시작하고, 개발자가 응답 객체를 폴링해 시간에 따른 상태를 확인하게 해 줘요. 응답 생성을 백그라운드에서 시작하려면 background를 true로 설정해 API 요청을 보내면 돼요.

Zero Data Retention(ZDR) 프로젝트의 백그라운드 요청은 store=false로 실행돼요. 비동기 실행과 폴링을 지원하기 위해 응답 데이터는 디스크에 약 10분 동안 임시 저장돼요.

수정된 남용 모니터링(강화형 포함)을 사용하는 프로젝트에서는, store를 생략하거나 true로 설정한 포그라운드 요청이 표준 보존을 따릅니다. 백그라운드 응답은 store=true를 명시적으로 제공한 경우에만 폴링 기간 이후에도 보존돼요. 백그라운드 요청에서 store를 생략하거나 false로 설정하면 응답은 약 10분 후 삭제돼요.

출처: 문서

본문

백그라운드에서 응답 생성하기

우주에서 수달에 관한 아주 긴 소설을 쓰는 예시를 볼게요.

curl https://api.openai.com/v1/responses \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ***" \
-d '{
  "model": "gpt-6-astra",
  "input": "Write a very long novel about otters in space.",
  "background": true
}'
import OpenAI from "openai";
const client = new OpenAI();

const resp = await client.responses.create({
  model: "gpt-6-astra",
  input: "Write a very long novel about otters in space.",
  background: true,
});

console.log(resp.status);
from openai import OpenAI

client = OpenAI()

resp = client.responses.create(
    model="gpt-6-astra",
    input="Write a very long novel about otters in space.",
    background=True,
)

print(resp.status)
package main

import (
	"context"
	"fmt"

	"github.com/openai/openai-go/v3"
	"github.com/openai/openai-go/v3/responses"
)

func main() {
	client := openai.NewClient()

	response, err := client.Responses.New(context.Background(), responses.ResponseNewParams{
		Model:      "gpt-6-astra",
		Background: openai.Bool(true),
		Input: responses.ResponseNewParamsInputUnion{
			OfString: openai.String("Write a very long novel about otters in space."),
		},
	})
	if err != nil {
		panic(err)
	}

	fmt.Println(response.Status)
}
import com.openai.client.OpenAIClient;
import com.openai.client.okhttp.OpenAIOkHttpClient;
import com.openai.models.responses.ResponseCreateParams;

ResponseCreateParams params =
    ResponseCreateParams.builder()
        .model("gpt-6-astra")
        .input("Write a detailed market analysis.")
        .background(true)
        .build();

var response = client.responses().create(params);
System.out.println(response.status().orElseThrow());
using OpenAI.Responses;
#pragma warning disable OPENAI001

string key = Environment.GetEnvironmentVariable("OPENAI_API_KEY")!;
ResponsesClient client = new(key);

CreateResponseOptions options = new()
{
    Model = "gpt-6-astra",
    BackgroundModeEnabled = true,
};
options.InputItems.Add(
    ResponseItem.CreateUserMessageItem("Write a very long novel about otters in space.")
);

ResponseResult response = await client.CreateResponseAsync(options);
Console.WriteLine(response.Status);
require "openai"

client = OpenAI::Client.new
response = client.responses.create(
  model: "gpt-6-astra",
  input: "Write a detailed market analysis.",
  background: true
)

puts(response.status)

백그라운드 응답 폴링하기

백그라운드 요청의 상태를 확인하려면 Responses의 GET 엔드포인트를 사용해요. 요청이 queued 또는 in_progress 상태인 동안 계속 폴링하고, 그 상태를 벗어나면 최종(terminal) 상태에 도달한 거예요.

백그라운드에서 실행 중인 응답 조회하는 예시를 볼게요.

curl https://api.openai.com/v1/responses/resp_123 \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer ***"
import OpenAI from "openai";
const client = new OpenAI();

let resp = await client.responses.create({
  model: "gpt-6-astra",
  input: "Write a very long novel about otters in space.",
  background: true,
});

while (resp.status === "queued" || resp.status === "in_progress") {
  console.log("Current status: " + resp.status);
  await new Promise((resolve) => setTimeout(resolve, 2000)); // wait 2 seconds
  resp = await client.responses.retrieve(resp.id);
}

console.log("Final status: " + resp.status + "\nOutput:\n" + resp.output_text);
from openai import OpenAI
from time import sleep

client = OpenAI()

resp = client.responses.create(
    model="gpt-6-astra",
    input="Write a very long novel about otters in space.",
    background=True,
)

while resp.status in {"queued", "in_progress"}:
    print(f"Current status: {resp.status}")
    sleep(2)
    resp = client.responses.retrieve(resp.id)

print(f"Final status: {resp.status}\nOutput:\n{resp.output_text}")
package main

import (
	"context"
	"fmt"
	"time"

	"github.com/openai/openai-go/v3"
	"github.com/openai/openai-go/v3/responses"
)

func main() {
	client := openai.NewClient()

	response, err := client.Responses.New(context.Background(), responses.ResponseNewParams{
		Model:      "gpt-6-astra",
		Background: openai.Bool(true),
		Input: responses.ResponseNewParamsInputUnion{
			OfString: openai.String("Write a very long novel about otters in space."),
		},
	})
	if err != nil {
		panic(err)
	}

	for response.Status == "queued" || response.Status == "in_progress" {
		fmt.Println("Current status:", response.Status)
		time.Sleep(2 * time.Second)
		response, err = client.Responses.Get(context.Background(), response.ID, responses.ResponseGetParams{})
		if err != nil {
			panic(err)
		}
	}

	fmt.Printf("Final status: %s\nOutput:\n%s\n", response.Status, response.OutputText())
}
import com.openai.client.OpenAIClient;
import com.openai.client.okhttp.OpenAIOkHttpClient;
import com.openai.models.responses.ResponseCreateParams;
import com.openai.models.responses.ResponseStatus;

ResponseCreateParams params =
    ResponseCreateParams.builder()
        .model("gpt-6-astra")
        .input("Write a very long novel about otters in space.")
        .background(true)
        .build();

var response = client.responses().create(params);
while (response.status().filter(ResponseStatus.QUEUED::equals).isPresent()
    || response.status().filter(ResponseStatus.IN_PROGRESS::equals).isPresent()) {
  System.out.println("Current status: " + response.status().orElseThrow());
  Thread.sleep(1000);
  response = client.responses().retrieve(response.id());
}
System.out.println("Final status: " + response.status().orElseThrow());
response.output().stream()
    .flatMap(item -> item.message().stream())
    .flatMap(message -> message.content().stream())
    .flatMap(content -> content.outputText().stream())
    .forEach(text -> System.out.println(text.text()));
using OpenAI.Responses;
#pragma warning disable OPENAI001

string key = Environment.GetEnvironmentVariable("OPENAI_API_KEY")!;
ResponsesClient client = new(key);

CreateResponseOptions options = new()
{
    Model = "gpt-6-astra",
    BackgroundModeEnabled = true,
};
options.InputItems.Add(
    ResponseItem.CreateUserMessageItem("Write a very long novel about otters in space.")
);

ResponseResult created = await client.CreateResponseAsync(options);
ResponseResult response = await client.GetResponseAsync(created.Id);
while (response.Status is ResponseStatus.Queued or ResponseStatus.InProgress)
{
    await Task.Delay(TimeSpan.FromSeconds(1));
    response = await client.GetResponseAsync(response.Id);
}
if (response.Status != ResponseStatus.Completed)
{
    throw new InvalidOperationException($"Background response ended with status: {response.Status}");
}
Console.WriteLine($"Status: {response.Status}");
Console.WriteLine(response.GetOutputText());
require "openai"

client = OpenAI::Client.new
response = client.responses.create(
  model: "gpt-6-astra",
  input: "Write a very long novel about otters in space.",
  background: true
)

while [:queued, :in_progress].include?(response.status)
  puts("Current status: #{response.status}")
  sleep(2)
  response = client.responses.retrieve(response.id)
end

puts("Final status: #{response.status}")
puts(response.output_text)

백그라운드 응답 취소하기

진행 중인 응답을 이렇게 취소할 수도 있어요.

curl -X POST https://api.openai.com/v1/responses/resp_123/cancel \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer ***"
import OpenAI from "openai";
const client = new OpenAI();

const resp = await client.responses.cancel("resp_123");

console.log(resp.status);
import os

from openai import OpenAI

response_id = os.environ["OPENAI_RESPONSE_ID"]
client = OpenAI()

resp = client.responses.cancel(response_id)

print(resp.status)
package main

import (
	"context"
	"fmt"

	"github.com/openai/openai-go/v3"
)

func main() {
	client := openai.NewClient()

	canceled, err := client.Responses.Cancel(context.Background(), "resp_123")
	if err != nil {
		panic(err)
	}

	fmt.Println(canceled.Status)
}
import com.openai.client.OpenAIClient;
import com.openai.client.okhttp.OpenAIOkHttpClient;

String responseId = "resp_123";

var response = client.responses().cancel(responseId);

System.out.println(response.status());
using OpenAI.Responses;
#pragma warning disable OPENAI001

string key = Environment.GetEnvironmentVariable("OPENAI_API_KEY")!;
ResponsesClient client = new(key);

// Replace this illustrative ID with the background response to cancel.
string responseId = "resp_123";

ResponseResult response = await client.CancelResponseAsync(responseId);
Console.WriteLine(response.Status);
require "openai"

client = OpenAI::Client.new
response = client.responses.cancel("resp_123")
puts(response.status)

취소를 두 번 하는 것은 멱등(idempotent)이에요. 이후 호출은 그냥 최종 Response 객체를 돌려줘요.

백그라운드 응답 스트리밍하기

백그라운드 Response를 만들고 바로 스트리밍 이벤트를 받기 시작할 수 있어요. 클라이언트가 스트림을 끊을 것으로 예상되지만 나중에 다시 이어받고 싶을 때 유용해요. 이렇게 하려면 background와 stream을 모두 true로 설정해 Response를 만들면 돼요. 각 스트리밍 이벤트에서 받는 sequence_number에 해당하는 "커서(cursor)"를 추적해 두는 편이 좋아요.

현재 백그라운드 응답에서 첫 토큰을 받는 데 걸리는 시간은 동기식보다 길어요. 이 지연 격차를 줄이기 위해 작업 중이에요.

백그라운드 응답을 생성하고 스트리밍하는 예시를 볼게요.

curl https://api.openai.com/v1/responses \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ***" \
-d '{
  "model": "gpt-6-astra",
  "input": "Write a very long novel about otters in space.",
  "background": true,
  "stream": true
}'

// To resume:
curl "https://api.openai.com/v1/responses/resp_123?stream=true&starting_after=42" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ***"
import OpenAI from "openai";
const client = new OpenAI();

const stream = await client.responses.create({
  model: "gpt-6-astra",
  input: "Write a very long novel about otters in space.",
  background: true,
  stream: true,
});

let cursor = null;
for await (const event of stream) {
  console.log(event);
  cursor = event.sequence_number;
}

// If the connection drops, you can resume streaming from the last cursor (SDK support coming soon):
// const resumedStream = await client.responses.stream(resp.id, { starting_after: cursor });
// for await (const event of resumedStream) { ... }
from openai import OpenAI

client = OpenAI()

# Fire off an async response but also start streaming immediately
stream = client.responses.create(
    model="gpt-6-astra",
    input="Write a very long novel about otters in space.",
    background=True,
    stream=True,
)

cursor = None
for event in stream:
    print(event)
    cursor = event.sequence_number

# If your connection drops, the response continues running and you can reconnect:
# SDK support for resuming the stream is coming soon.
# for event in client.responses.stream(resp.id, starting_after=cursor):
#     print(event)
package main

import (
	"context"
	"fmt"

	"github.com/openai/openai-go/v3"
	"github.com/openai/openai-go/v3/responses"
)

func main() {
	client := openai.NewClient()

	stream := client.Responses.NewStreaming(context.Background(), responses.ResponseNewParams{
		Model:      "gpt-6-astra",
		Background: openai.Bool(true),
		Input: responses.ResponseNewParamsInputUnion{
			OfString: openai.String("Write a very long novel about otters in space."),
		},
	})
	var cursor int64
	var responseID string
	for stream.Next() {
		event := stream.Current()
		fmt.Println(event.Type)
		cursor = event.SequenceNumber
		if event.Response.ID != "" {
			responseID = event.Response.ID
		}
	}
	if err := stream.Err(); err != nil {
		panic(err)
	}
	fmt.Printf("response %s last cursor %d\n", responseID, cursor)

	// If the connection drops, resume streaming from the last cursor:
	// resumed := client.Responses.GetStreaming(
	// 	context.Background(),
	// 	responseID,
	// 	responses.ResponseGetParams{StartingAfter: openai.Int(cursor)},
	// )
	// for resumed.Next() {
	// 	fmt.Println(resumed.Current().Type)
	// }
}
import com.fasterxml.jackson.databind.json.JsonMapper;
import com.openai.client.OpenAIClient;
import com.openai.client.okhttp.OpenAIOkHttpClient;
import com.openai.core.http.StreamResponse;
import com.openai.models.responses.ResponseCreateParams;
import com.openai.models.responses.ResponseRetrieveParams;
import com.openai.models.responses.ResponseStreamEvent;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;

ResponseCreateParams params =
    ResponseCreateParams.builder()
        .model("gpt-6-astra")
        .input("Write a very long novel about otters in space.")
        .background(true)
        .build();

AtomicLong lastSequenceNumber = new AtomicLong(-1);
AtomicReference<String> responseId = new AtomicReference<>("");
AtomicBoolean streamCompleted = new AtomicBoolean(false);
JsonMapper json = new JsonMapper();
try (StreamResponse<ResponseStreamEvent> stream = client.responses().createStreaming(params)) {
  stream.stream()
      .forEach(
          event -> {
            lastSequenceNumber.set(json.valueToTree(event).path("sequence_number").asLong());
            event
                .created()
                .ifPresent(
                    created -> {
                      responseId.set(created.response().id());
                      System.out.println("response.created");
                    });
            event
                .outputTextDelta()
                .ifPresent(
                    delta -> {
                      System.out.println("response.output_text.delta");
                    });
            event
                .completed()
                .ifPresent(
                    completed -> {
                      streamCompleted.set(true);
                      System.out.println("response.completed");
                    });
          });
}
System.out.println(
    "Response " + responseId.get() + "; last sequence number " + lastSequenceNumber.get());
if (!streamCompleted.get()) {
  try (StreamResponse<ResponseStreamEvent> resumed =
      client
          .responses()
          .retrieveStreaming(
              ResponseRetrieveParams.builder()
                  .responseId(responseId.get())
                  .startingAfter(lastSequenceNumber.get())
                  .build())) {
    resumed.stream()
        .forEach(
            event ->
                event.outputTextDelta().ifPresent(delta -> System.out.println(delta.delta())));
  }
}
using OpenAI.Responses;
#pragma warning disable OPENAI001

string key = Environment.GetEnvironmentVariable("OPENAI_API_KEY")!;
ResponsesClient client = new(key);

CreateResponseOptions options = new()
{
    Model = "gpt-6-astra",
    BackgroundModeEnabled = true,
    StreamingEnabled = true,
};
options.InputItems.Add(
    ResponseItem.CreateUserMessageItem("Write a very long novel about otters in space.")
);

string? responseId = null;
int lastSequenceNumber = -1;
bool completed = false;

void HandleUpdate(StreamingResponseUpdate update)
{
    lastSequenceNumber = update.SequenceNumber;
    switch (update)
    {
        case StreamingResponseCreatedUpdate created:
            responseId = created.Response.Id;
            break;
        case StreamingResponseOutputTextDeltaUpdate text:
            Console.Write(text.Delta);
            break;
        case StreamingResponseCompletedUpdate:
            completed = true;
            break;
        case StreamingResponseFailedUpdate:
            throw new InvalidOperationException("The background response failed.");
        case StreamingResponseIncompleteUpdate:
            throw new InvalidOperationException("The background response was incomplete.");
        case StreamingResponseErrorUpdate error:
            throw new InvalidOperationException($"The response stream failed: {error.Message}");
    }
}

try
{
    await foreach (
        StreamingResponseUpdate update in client.CreateResponseStreamingAsync(options)
    )
    {
        HandleUpdate(update);
    }
}
catch (Exception error)
    when (error is HttpRequestException or IOException && responseId is not null)
{
    // The background response continues after its streaming connection is interrupted.
}

if (!completed)
{
    if (responseId is null)
    {
        throw new InvalidOperationException("The response stream ended before providing its ID.");
    }

    GetResponseOptions resumeOptions = new(responseId)
    {
        StartingAfter = lastSequenceNumber,
        StreamingEnabled = true,
    };
    await foreach (StreamingResponseUpdate update in client.GetResponseStreamingAsync(resumeOptions))
    {
        HandleUpdate(update);
    }

    if (!completed)
    {
        throw new InvalidOperationException(
            "The resumed response stream ended before the background response completed."
        );
    }
}
require "openai"

client = OpenAI::Client.new
stream = client.responses.stream(
  model: "gpt-6-astra",
  input: "Write a very long novel about otters in space.",
  background: true
)

last_sequence_number = -1
response_id = ""
stream.each do |event|
  puts(event.type)
  last_sequence_number = event.sequence_number || last_sequence_number
  if event.is_a?(OpenAI::Models::Responses::ResponseCreatedEvent)
    response_id = event.response.id
  end
end

puts("Response #{response_id}; last sequence number #{last_sequence_number}")

# If the connection drops, resume from the last sequence number:
# client.responses.stream(response_id: response_id, starting_after: last_sequence_number).each do |event|
#   puts(event.type)
# end

제한 사항

  1. 백그라운드 요청은 store=false를 사용할 수 있지만, 비동기 실행과 폴링을 지원하기 위해 응답 데이터는 임시 저장돼요.
  2. 동기 응답을 취소하려면 연결을 끊으면 돼요.
  3. stream=true로 만든 백그라운드 응답에서만 새 스트림을 시작할 수 있어요.

더 알아보기 (Learn more)