Connecting Vector Stores to Responses API Requests
Attaching a Vector Store to a Request
Lesson 1 showed the minimal shape of a file_search-enabled request. This lesson goes deeper into the mechanics: how to attach one or several stores, how to read what was actually retrieved, and how to control the request's behavior beyond the default.
from openai import OpenAI
client = OpenAI()
response = client.responses.create(
model="gpt-5.6-terra",
input="How many days of notice do I need to give to cancel my contract?",
tools=[
{
"type": "file_search",
"vector_store_ids": ["vs_legal_policies_68f2"],
}
],
)
print(response.output_text)
The tools parameter is a list because a single request can, in principle, be given multiple tools (file_search alongside web_search, for example — the subject of Lesson 9). Each tool dictionary declares its type and its own configuration. For file_search, the required configuration is vector_store_ids, a list of one or more store IDs the model is allowed to search within for this request. The model decides, based on the input, whether it needs to invoke the tool at all — a request whose input is unrelated to anything in the store (a casual greeting, for instance) may not trigger a search.
Note: The precise structure of the
toolslist entry forfile_search, including whether additional optional keys are supported, is version-specific. Verify the current tool schema against official OpenAI documentation before finalizing production request code.
Searching Across Multiple Vector Stores
response = client.responses.create(
model="gpt-5.6-terra",
input="What is our policy on remote work, and does it affect expense reimbursement?",
tools=[
{
"type": "file_search",
"vector_store_ids": [
"vs_hr_policies_68f2",
"vs_finance_policies_71a9",
],
}
],
)
print(response.output_text)
This example attaches two vector stores to a single file_search tool call, because the user's question genuinely spans two domains — HR policy and finance policy — that Lesson 2 recommended keeping as separate stores for relevance and access-control reasons. The retrieval step searches across chunks from both stores and returns the best matches regardless of which store they came from. This is the payoff of the organizational discipline from Lesson 2: you get precise, narrowly-scoped stores most of the time, but can still combine them ad hoc for a request that legitimately needs both, without merging the underlying data.
A tradeoff worth understanding: searching across more stores means a larger candidate pool for the top-K similarity search, which can slightly dilute precision if the stores are only tangentially related to the query. Only combine stores that are genuinely relevant to the assistant's actual scope, not defensively "just in case."
Reading the Structured Response
response.output_text gives you the final generated answer as a string, but the full response object carries more detail about what the model actually did, including which chunks were retrieved and cited:
response = client.responses.create(
model="gpt-5.6-terra",
input="What is our refund policy for annual subscriptions?",
tools=[{"type": "file_search", "vector_store_ids": ["vs_legal_policies_68f2"]}],
)
for item in response.output:
if item.type == "file_search_call":
print("Search queries used:", item.queries)
print("Search status:", item.status)
elif item.type == "message":
for content_block in item.content:
if hasattr(content_block, "annotations"):
for annotation in content_block.annotations:
print("Cited file:", annotation.file_id)
print("Cited filename:", getattr(annotation, "filename", None))
Note: The exact item
typevalues (file_search_call,message), the fields on each (queries,status), and the annotation structure for citations are all specific to the current API version. Confirm these field names against official documentation before building parsing logic that depends on them.
The response.output list contains every step the model took to produce its answer, not just the final text. A file_search_call item represents the tool invocation itself — it tells you what queries were actually sent to the vector store search (which may be reformulated from the user's original input) and whether the search succeeded. A message item is the model's actual textual output, and its content blocks can carry annotations — structured citations pointing back to the specific file (and often the specific chunk) that supported a piece of text. This is what makes file_search answers auditable: you can show a user "this answer was based on section 4.2 of the Refund Policy PDF" instead of an unverifiable claim.
Reading this structured output rather than just the final text matters for two production concerns covered later in this unit: building a UI that shows citations to end users (Lesson 6), and detecting when the model answered without solid evidence (Lesson 8).
Combining File Search With Other Instructions
A file_search tool doesn't replace your system instructions — it augments the context the model reasons over. You still control tone, format, and behavior through the instructions parameter, exactly as in a request without any tools:
response = client.responses.create(
model="gpt-5.6-terra",
instructions=(
"You are a customer support assistant. Answer only using information "
"found via file search. If the documents don't contain an answer, "
"say you don't have that information rather than guessing."
),
input="Can I get a refund after 90 days?",
tools=[{"type": "file_search", "vector_store_ids": ["vs_legal_policies_68f2"]}],
)
print(response.output_text)
This is a small but important pattern: the instructions field is where you explicitly tell the model what to do when retrieval doesn't produce a confident answer, because the model's default behavior — filling gaps with generally plausible-sounding text — is exactly what you don't want from a grounded knowledge assistant. Lesson 8 goes into much more depth on detecting and handling this "no good evidence found" case, but the instruction shown here is the first line of defense, and it costs nothing to include by default in every file-search-backed assistant you build.
Passing Conversation History Alongside File Search
Because file_search is just another tool available within a normal Responses API call, it composes naturally with multi-turn conversations. If you're managing conversation state manually (rather than using a persistent conversation object), you pass prior turns as part of input just as you would without any tools attached:
conversation = [
{"role": "user", "content": "What's included in the premium plan?"},
]
first_response = client.responses.create(
model="gpt-5.6-terra",
input=conversation,
tools=[{"type": "file_search", "vector_store_ids": ["vs_product_docs_55c1"]}],
)
conversation.append({"role": "assistant", "content": first_response.output_text})
conversation.append({"role": "user", "content": "And does that include priority support?"})
second_response = client.responses.create(
model="gpt-5.6-terra",
input=conversation,
tools=[{"type": "file_search", "vector_store_ids": ["vs_product_docs_55c1"]}],
)
print(second_response.output_text)
The follow-up question "does that include priority support" is only answerable because the conversation history is included in input — the model uses it to resolve "that" as referring to the premium plan mentioned earlier, then issues a new file_search query informed by that resolved context. Each turn's retrieval is independent; the vector store search itself has no memory of prior turns, so it's the model's reasoning over the full conversation history that carries context across turns, not the search index.
Common Mistakes
Forgetting that output_text hides tool activity, which leads developers to assume no retrieval happened simply because they never inspected response.output — always check the structured output when you need to know whether and how file_search was actually invoked.
Attaching every available vector store to every request "to be safe," which dilutes retrieval precision and increases latency without a corresponding benefit — attach only the stores relevant to that specific assistant or conversation's scope.
Not giving explicit instructions for the no-evidence case, leaving the model's default behavior (generating a plausible-sounding but ungrounded answer) in place — always instruct the model on what to do when file search doesn't turn up a confident answer.
Best Practices
Always inspect response.output during development, not just output_text, so you can see exactly which queries were run and which files were cited before you ship an assistant to real users.
Scope vector_store_ids per assistant or per feature, not globally. A support bot, a legal-review assistant, and an onboarding assistant should typically reference different, narrowly-scoped sets of stores even if they're all built on the same underlying model and code path.
Pair every file_search tool configuration with explicit instructions about grounding and uncertainty. The tool retrieves evidence; your instructions determine whether the model is disciplined about using only that evidence.