gcloud CLI initialized, Python 3, an OpenAI API key (starts with sk-), and the roles/owner or roles/editor role on the project plus roles/secretmanager.adminScope note: Google Cloud does not resell OpenAI API keys. The pattern below runs your workload inside Google Cloud and calls the OpenAI API from it, keeping the key in Secret Manager instead of source code or environment files. Whether a given OpenAI model is also offered as a managed endpoint in Vertex AI Model Garden changes over time — verify current partner-model availability against the documentation before choosing that path.

gcloud CLI installed and initialized (gcloud version must print a version string)python3 --version) for the smoke test scriptExample: a working starting state looks like this.
$ gcloud version
Google Cloud SDK 5xx.x.x
$ gcloud config list
[core]
project = ai-openai-gw
account = you@example.com
Why this works: the gcloud CLI stores the active project and account per user, so every later command applies to the right project without repeating the --project flag.
What to do: create (or select) the project, link billing, and enable the three APIs the rest of the guide depends on: Secret Manager, Cloud Run, and Cloud Build.
gcloud projects create ai-openai-gw --name="OpenAI via GCP"
gcloud config set project ai-openai-gw
# Find your billing account ID
gcloud beta billing accounts list
# Link it — replace the placeholder with the ID from the previous command
gcloud beta billing projects link ai-openai-gw \
--billing-account=<BILLING_ACCOUNT_ID>
gcloud services enable \
secretmanager.googleapis.com \
run.googleapis.com \
cloudbuild.googleapis.com
Placeholders: <BILLING_ACCOUNT_ID> is the value in the ACCOUNT_ID column, formatted XXXXXX-XXXXXX-XXXXXX. Replace ai-openai-gw with your own project ID if it is already taken — project IDs are globally unique.
Why: no Google Cloud API is callable until it is enabled on the project, and a project without a billing account rejects every billable call. Enabling only three services keeps the surface small and the audit log readable.
How to check: list what is actually enabled.
gcloud services list --enabled \
--filter="config.name:(secretmanager.googleapis.com OR run.googleapis.com OR cloudbuild.googleapis.com)"
Expected output — three rows, each with STATE: ENABLED:
NAME TITLE
cloudbuild.googleapis.com Cloud Build API
run.googleapis.com Cloud Run Admin API
secretmanager.googleapis.com Secret Manager API
If a row is missing, the enable call failed; rerun it and read the error text.

What to do: upload the OpenAI key as a secret version, create a dedicated service account, and bind only the Secret Accessor role to it.
# 1. Read the key into a shell variable without echoing it
read -rs OPENAI_API_KEY
# 2. Create the secret and its first version
printf '%s' "$OPENAI_API_KEY" \
| gcloud secrets create openai-api-key \
--data-file=- \
--replication-policy=automatic
# 3. Create a least-privilege identity for the workload
gcloud iam service-accounts create openai-runner \
--display-name="OpenAI runner"
# 4. Allow that identity to read this one secret
gcloud secrets add-iam-policy-binding openai-api-key \
--member="serviceAccount:openai-runner@ai-openai-gw.iam.gserviceaccount.com" \
--role="roles/secretmanager.secretAccessor"
Then deploy a container that receives the secret as a reference, not as a literal value:
gcloud run deploy openai-proxy \
--image=<REGION>-docker.pkg.dev/ai-openai-gw/apps/openai-proxy:latest \
--region=<REGION> \
--service-account=openai-runner@ai-openai-gw.iam.gserviceaccount.com \
--set-secrets=OPENAI_API_KEY=openai-api-key:latest
Replace <REGION> with a region you enabled, for example us-central1.
Why: --set-env-vars OPENAI_API_KEY=sk-... writes the literal key into the revision configuration, where anyone holding roles/viewer on the project can read it. --set-secrets stores only a reference (openai-api-key:latest), and the container fetches the value at runtime using the service account's identity. A dedicated service account means a compromised container can read exactly one secret and nothing else.
How to check: confirm the secret exists, has one version, and that the stored value looks like a key.
gcloud secrets versions list openai-api-key
gcloud secrets versions access latest --secret=openai-api-key | head -c 7
Expected: one row in STATE: enabled, then sk- (or sk-proj for project-scoped keys) with no trailing newline. If you see the key truncated mid-string or followed by a blank line, you uploaded it with echo instead of printf — recreate the version.
If you plan to route traffic to several providers behind one endpoint, the pattern in How to combine multiple AI providers behind one OpenAI-compatible router extends this step cleanly: one secret per provider, one service account per deployment.

What to do: run two checks — one directly against the OpenAI API from your machine, one through the deployed Cloud Run service.
# Check A: the stored secret is a valid OpenAI credential
export OPENAI_API_KEY="$(gcloud secrets versions access latest --secret=openai-api-key)"
curl -sS https://api.openai.com/v1/models \
-H "Authorization: Bearer $OPENAI_API_KEY" | head -c 300
Expected output starts with {"object":"list","data":[{"id":"... — a JSON list of model IDs available to your account. Note the IDs from that list rather than hard-coding a model name in your app; availability depends on your OpenAI account tier.
# Check B: the deployed service is up and bound to the right identity
gcloud run services describe openai-proxy \
--region=<REGION> \
--format="value(status.url,status.conditions[0].status)"
Expected: the service URL followed by True. A False here means the revision never became ready — read the logs with gcloud run services logs read openai-proxy --region=<REGION>.
Then send one request through your service:
SERVICE_URL=<YOUR_SERVICE_URL>
curl -sS "$SERVICE_URL/v1/chat/completions" \
-H "Content-Type: application/json" \
-d '{"model":"<MODEL_ID_FROM_STEP_3_CHECK_A>",
"messages":[{"role":"user","content":"Reply with the single word: ok"}]}'
Why: Check A isolates the credential from your infrastructure — if it fails, the problem is the key, not Google Cloud. Check B isolates the deployment from the credential. Running them in that order means a failure points at exactly one layer.
How to check: success looks like an HTTP 200 with a choices array in the body:
{"id":"chatcmpl-...","object":"chat.completion",
"choices":[{"index":0,"message":{"role":"assistant","content":"ok"},
"finish_reason":"stop"}]}
A 403 from the Cloud Run URL means the service requires authentication — add -H "Authorization: Bearer $(gcloud auth print-identity-token)".
For running OpenAI-compatible endpoints from a coding agent instead of curl, see How to connect a local LLM to Claude Code: llama-server and an OpenAI-compatible API — the request shape is the same, only the base URL changes.
Problem. The service account cannot read the secret
ERROR: (gcloud.secrets.versions.access) PERMISSION_DENIED: Permission 'secretmanager.versions.access' denied for resource 'projects/ai-openai-gw/secrets/openai-api-key'.gcloud secrets get-iam-policy openai-api-key
gcloud secrets add-iam-policy-binding openai-api-key \
--member="serviceAccount:openai-runner@ai-openai-gw.iam.gserviceaccount.com" \
--role="roles/secretmanager.secretAccessor"
Problem. OpenAI rejects the key that works in the dashboard
{"error": {"message": "Incorrect API key provided: sk-...xxxx. You can find your API key at https://platform.openai.com/account/api-keys.", "type": "invalid_request_error", "param": null, "code": "invalid_api_key"}}echo "$KEY" | gcloud secrets create ... on Windows/WSL, or from copy-pasting a key with a line break.printf '%s' and re-run the check.printf '%s' "$OPENAI_API_KEY" | gcloud secrets versions add openai-api-key --data-file=-
gcloud secrets versions access latest --secret=openai-api-key | wc -c
The byte count must equal the key length exactly — no extra 1 for a newline.
Problem. Requests fail with a quota error while the key is valid
{"error": {"message": "You exceeded your current quota, please check your plan and billing details.", "type": "insufficient_quota", "code": "insufficient_quota"}} or HTTP RESOURCE_EXHAUSTED from Cloud Run.Example: a minimal retry wrapper that only retries on quota/rate errors:
import time, openai
def chat(client, **kwargs):
for attempt in range(4):
try:
return client.chat.completions.create(**kwargs)
except openai.RateLimitError:
time.sleep(2 ** attempt)
raise RuntimeError("quota still exhausted after 4 attempts")
You now have the OpenAI key stored as a Secret Manager version, a least-privilege service account that can read only that secret, and a Cloud Run service verified end to end with a live chat completion. Next, add alerting on secretmanager.versions.access audit logs so you notice unexpected reads, and if you add a second provider, front both with the router guide linked above rather than duplicating credentials in each client.
Do I need a Google Cloud account to use the OpenAI API?
No. The key is issued by OpenAI and billed by OpenAI; Google Cloud is used here only to store the key securely and to host the code that calls it.
Where should I put the API key in Google Cloud?
In Secret Manager, mounted into the workload as a reference via --set-secrets. Never in --set-env-vars, a Dockerfile, or a committed .env file.
Can I use several OpenAI keys for different environments?
Yes — create one secret per environment (openai-api-key-dev, openai-api-key-prod) and bind each service account to exactly one of them. That way a staging compromise cannot spend production quota.
How do I rotate the key without downtime?
Add a new version with gcloud secrets versions add openai-api-key --data-file=-, redeploy or restart the service, then disable the old version with gcloud secrets versions disable <VERSION> --secret=openai-api-key. The :latest reference picks up the new version on the next cold start.