Billing labels on Vertex AI batch embedding jobs never reach Cloud Billing
Environment: google-genai 2.22.0, Python 3.13, Vertex AI (vertexai=True), us-central1, model gemini-embedding-001
What you're trying to do
We split our Vertex AI costs by custom metadata labels.
- Real-time calls work:
generateContent with GenerateContentConfig(labels=...), and gemini-embedding-001 embed_content (which the SDK sends to :predict) with labels added through http_options.extra_body. Both labels show up in Cloud Billing.
- Batch embeddings don't: for the same model through
client.batches.create(...), no label has ever appeared in billing, whether set on the job or in the JSONL rows.
Questions:
- Is there a supported way to attach billing labels to a Vertex batch embedding job?
- Batch rows accept
request.labels, while every other unknown key fails the row. Is that intended, and should it reach billing?
- Could
CreateBatchJobConfig and EmbedContentConfig get a labels field, as GenerateContentConfig and GenerateImagesConfig have? Could BatchJob keep labels when reading a job back? Today it drops them.
What code you've already tried
1. Real-time embedding: works
The label shows in billing.
await client.aio.models.embed_content(
model='gemini-embedding-001',
contents='...',
config=types.EmbedContentConfig(
task_type='RETRIEVAL_DOCUMENT',
output_dimensionality=768,
http_options=types.HttpOptions(extra_body={'labels': {'testing': 'rt_predict'}}),
),
)
Request as sent (intercepted):
POST https://us-central1-aiplatform.googleapis.com/v1beta1/projects/PROJECT/locations/us-central1/publishers/google/models/gemini-embedding-001:predict
{"instances": [{"content": "...", "task_type": "RETRIEVAL_DOCUMENT"}],
"parameters": {"outputDimensionality": 768},
"labels": {"testing": "rt_predict"}}
-> 200
Note: PredictRequest.labels is documented as "for Imagen billing usage only", but these labels do reach billing for this model.
2. Batch job labels: the job stores them, billing doesn't show them
CreateBatchJobConfig has no labels field, so we add them through extra_body:
await client.aio.batches.create(
model='gemini-embedding-001',
src=types.BatchJobSource(gcs_uri=['gs://BUCKET/input.jsonl'], format='jsonl'),
config=types.CreateBatchJobConfig(
display_name='label_test',
dest='gs://BUCKET/outputs/',
http_options=types.HttpOptions(extra_body={'labels': {'testing': 'j_labels'}}),
),
)
The job is created, a REST GET on the job returns "labels": {"testing": "j_labels"}, and the rows embed fine. No testing label appears on the cost.
3. Per-row labels
We tried every placement we could think of, one job per placement. Rows look like this:
{"key": "1", "request": {"content": {"parts": [{"text": "..."}]}, "output_dimensionality": 768, "task_type": "RETRIEVAL_DOCUMENT", "labels": {"testing": "r_request"}}}
| Label placement in the JSONL line |
Result |
request.labels |
row embeds fine (accepted), not in billing |
request.content.labels |
row error: no such field: 'labels' (Content) |
request.content.parts[0].labels |
row error: no such field: 'labels' (Part) |
request.embed_content_config.labels / request.embedContentConfig.labels |
row error: no such field: 'labels' (EmbedContentConfig) |
request.metadata.labels |
row error: no such field: 'metadata' |
request.request_labels |
row error: no such field: 'request_labels' |
labels object beside request |
job fails (see errors below) |
labels as a JSON string beside request |
accepted as a pass-through column, not in billing |
flat request.content string (:predict-style) |
row error: can't parse into EmbedContentRequest |
Job-level placements:
| Placement on the job |
Result |
labels |
stored on the job, not in billing |
modelParameters.labels |
stored, not in billing |
inputConfig.labels, outputConfig.labels, instanceConfig.labels |
400 at creation |
4. Real-time embedContent with labels
This is the request type batch rows are parsed into. It is rejected on both v1 and v1beta1:
POST .../publishers/google/models/gemini-embedding-001:embedContent
{"content": {"parts": [{"text": "..."}]}, "taskType": "RETRIEVAL_DOCUMENT", "outputDimensionality": 768, "labels": {"testing": "rt_embed_content"}}
-> 400
Any error messages you're getting
No error where it matters: request.labels and job labels are accepted and the jobs succeed, but the labels never reach Cloud Billing.
Errors from the other placements:
Failed to parse JSON into proto: google.cloud.aiplatform.master.EmbedContentRequest with status: invalid JSON in google.cloud.aiplatform.master.EmbedContentRequest @ content: message google.cloud.aiplatform.master.Content, near 1:22 (offset 21): no such field: 'labels'
Failed to parse JSON into proto: google.cloud.aiplatform.master.EmbedContentRequest with status: invalid JSON in google.cloud.aiplatform.master.EmbedContentRequest, near 1:86 (offset 85): no such field: 'metadata'
The column or property "labels" in the specified input data is of unsupported type. Supported types for this column are: [STRING, INTEGER, FLOAT, BOOLEAN, TIMESTAMP, DATE, DATETIME, NUMERIC].
400 INVALID_ARGUMENT. Invalid JSON payload received. Unknown name "labels" at 'batch_prediction_job.input_config': Cannot find field.
400 INVALID_ARGUMENT. Invalid JSON payload received. Unknown name "labels": Cannot find field. (real-time :embedContent, v1 and v1beta1)
Billing labels on Vertex AI batch embedding jobs never reach Cloud Billing
Environment:
google-genai2.22.0, Python 3.13, Vertex AI (vertexai=True),us-central1, modelgemini-embedding-001What you're trying to do
We split our Vertex AI costs by custom metadata labels.
generateContentwithGenerateContentConfig(labels=...), andgemini-embedding-001embed_content(which the SDK sends to:predict) with labels added throughhttp_options.extra_body. Both labels show up in Cloud Billing.client.batches.create(...), no label has ever appeared in billing, whether set on the job or in the JSONL rows.Questions:
request.labels, while every other unknown key fails the row. Is that intended, and should it reach billing?CreateBatchJobConfigandEmbedContentConfigget alabelsfield, asGenerateContentConfigandGenerateImagesConfighave? CouldBatchJobkeeplabelswhen reading a job back? Today it drops them.What code you've already tried
1. Real-time embedding: works
The label shows in billing.
Request as sent (intercepted):
2. Batch job labels: the job stores them, billing doesn't show them
CreateBatchJobConfighas nolabelsfield, so we add them throughextra_body:The job is created, a REST
GETon the job returns"labels": {"testing": "j_labels"}, and the rows embed fine. Notestinglabel appears on the cost.3. Per-row labels
We tried every placement we could think of, one job per placement. Rows look like this:
{"key": "1", "request": {"content": {"parts": [{"text": "..."}]}, "output_dimensionality": 768, "task_type": "RETRIEVAL_DOCUMENT", "labels": {"testing": "r_request"}}}request.labelsrequest.content.labelsno such field: 'labels'(Content)request.content.parts[0].labelsno such field: 'labels'(Part)request.embed_content_config.labels/request.embedContentConfig.labelsno such field: 'labels'(EmbedContentConfig)request.metadata.labelsno such field: 'metadata'request.request_labelsno such field: 'request_labels'labelsobject besiderequestlabelsas a JSON string besiderequestrequest.contentstring (:predict-style)EmbedContentRequestJob-level placements:
labelsmodelParameters.labelsinputConfig.labels,outputConfig.labels,instanceConfig.labels4. Real-time
embedContentwith labelsThis is the request type batch rows are parsed into. It is rejected on both
v1andv1beta1:Any error messages you're getting
No error where it matters:
request.labelsand joblabelsare accepted and the jobs succeed, but the labels never reach Cloud Billing.Errors from the other placements: